├── .dockerignore ├── .github ├── actions │ └── release │ │ └── action.yml ├── release.yml └── workflows │ ├── ci.yml │ ├── release.yml │ └── tagpr.yml ├── .gitignore ├── .goreleaser.yml ├── .tagpr ├── DEPENDENCIES.md ├── LICENSE ├── Makefile ├── README.md ├── SPEC.md ├── cmd ├── completion.go ├── delete.go ├── gc.go ├── list.go ├── pull.go ├── push.go ├── root.go ├── show.go └── validation.go ├── examples ├── argo │ ├── Dockerfile │ ├── run.sh │ └── workflow.yaml └── k8s │ ├── pod.yaml │ └── run.sh ├── go.mod ├── go.sum ├── golangci.yml ├── main.go ├── pkg ├── ghost │ ├── delete.go │ ├── doc.go │ ├── git │ │ ├── check.go │ │ ├── conversion.go │ │ ├── doc.go │ │ ├── file.go │ │ ├── list.go │ │ ├── repo.go │ │ └── validation.go │ ├── list.go │ ├── pull.go │ ├── push.go │ ├── show.go │ └── types │ │ ├── branch.go │ │ ├── branchspec.go │ │ ├── listbranchspec.go │ │ └── workingenv.go └── util │ ├── common.go │ ├── errors │ ├── errors.go │ └── errors_test.go │ ├── exec.go │ ├── file.go │ ├── hash │ ├── hash.go │ └── hash_test.go │ ├── logrus_fields.go │ └── slice.go ├── release.Dockerfile ├── scripts └── license │ ├── add.py │ ├── check.py │ └── license_header.py └── test ├── e2e ├── e2e.go └── e2e_test.go └── util └── workdir.go /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor/** 2 | bin/** 3 | dist/** 4 | tmp/** 5 | .envrc 6 | -------------------------------------------------------------------------------- /.github/actions/release/action.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | description: release executables 3 | 4 | inputs: 5 | tag: 6 | description: check out the tag if not empty 7 | default: '' 8 | token: 9 | description: GitHub token 10 | required: true 11 | 12 | runs: 13 | using: composite 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | name: "checkout tag ${{ inputs.tag }}" 18 | if: "inputs.tag != ''" 19 | with: 20 | ref: refs/tags/${{ inputs.tag }} 21 | env: 22 | GITHUB_TOKEN: ${{ inputs.token }} 23 | - name: Set up QEMU for cross-platform image build 24 | uses: docker/setup-qemu-action@v2 25 | - name: Set up Go 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: ~1.19 29 | - name: Login to GitHub Container Registry 30 | uses: docker/login-action@v2 31 | with: 32 | registry: ghcr.io 33 | username: pfnet-research 34 | password: ${{ inputs.token }} 35 | - name: Run GoReleaser 36 | uses: goreleaser/goreleaser-action@v3 37 | with: 38 | version: v1.11.5 39 | args: release --rm-dist 40 | env: 41 | GITHUB_TOKEN: ${{ inputs.token }} 42 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - release-note/skip 5 | - tagpr 6 | categories: 7 | - title: "💣 Breaking Changes" 8 | labels: 9 | - release-note/breaking-change 10 | - title: "🚀 Features" 11 | labels: 12 | - release-note/feature 13 | - title: "🐛 Bug Fixes" 14 | labels: 15 | - release-note/bugfix 16 | - title: "📜 Documentation" 17 | labels: 18 | - release-note/document 19 | - title: "🧰 Maintenance" 20 | labels: 21 | - release-note/chore 22 | - title: "🔬 Other Changes" 23 | labels: 24 | - "*" 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags-ignore: [ "**" ] 7 | paths-ignore: [ "**.md"] 8 | pull_request: 9 | types: [opened, synchronize] 10 | paths-ignore: [ "**.md" ] 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: ~1.19 21 | - name: Check out 22 | uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | - name: Get dependencies 26 | run: go mod download 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v3 29 | with: 30 | version: v1.50.0 31 | args: --config golangci.yml 32 | - name: Build 33 | run: make install e2e 34 | - name: Test With Coverage 35 | run: make coverage 36 | - name: Send Coverage 37 | uses: shogo82148/actions-goveralls@v1 38 | with: 39 | path-to-profile: profile.cov 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This workflow runs when tagged manually. 3 | # It would be useful when tagpr workflow failed in some reason. 4 | # 5 | name: Release By Tagged Manually 6 | 7 | on: 8 | push: 9 | tags: ["v[0-9]+.[0-9]+.[0-9]+"] 10 | 11 | jobs: 12 | run: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: ./.github/actions/release 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/tagpr.yml: -------------------------------------------------------------------------------- 1 | # 2 | # tagpr master workflow 3 | # 4 | name: tagpr 5 | on: 6 | push: 7 | branches: ["master"] 8 | 9 | jobs: 10 | tagpr: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - id: tagpr 15 | name: Tagpr 16 | uses: Songmu/tagpr@v1 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | # If tagpr pushed tags, invoke release custom action manually. 21 | # It is because: 22 | # > When you use the repository's GITHUB_TOKEN to perform tasks, 23 | # > events triggered by the GITHUB_TOKEN, with the exception of 24 | # > workflow_dispatch and repository_dispatch, will not create 25 | # > a new workflow run. 26 | # ref: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow 27 | - name: "Release (only when tagged)" 28 | uses: ./.github/actions/release 29 | if: "steps.tagpr.outputs.tag != ''" 30 | with: 31 | tag: ${{ steps.tagpr.outputs.tag }} 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/f908e51bcf38ae5ede449c55189a7b25d8c507cc/Go.gitignore 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # vendoring 17 | vendor/** 18 | 19 | ### https://raw.github.com/github/gitignore/f908e51bcf38ae5ede449c55189a7b25d8c507cc/Python.gitignore 20 | 21 | # Byte-compiled / optimized / DLL files 22 | __pycache__/ 23 | *.py[cod] 24 | *$py.class 25 | 26 | # C extensions 27 | *.so 28 | 29 | # Distribution / packaging 30 | .Python 31 | build/ 32 | develop-eggs/ 33 | dist/ 34 | downloads/ 35 | eggs/ 36 | .eggs/ 37 | lib/ 38 | lib64/ 39 | parts/ 40 | sdist/ 41 | var/ 42 | wheels/ 43 | share/python-wheels/ 44 | *.egg-info/ 45 | .installed.cfg 46 | *.egg 47 | MANIFEST 48 | 49 | # PyInstaller 50 | # Usually these files are written by a python script from a template 51 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 52 | *.manifest 53 | *.spec 54 | 55 | # Installer logs 56 | pip-log.txt 57 | pip-delete-this-directory.txt 58 | 59 | # Unit test / coverage reports 60 | htmlcov/ 61 | .tox/ 62 | .nox/ 63 | .coverage 64 | .coverage.* 65 | .cache 66 | nosetests.xml 67 | coverage.xml 68 | *.cover 69 | .hypothesis/ 70 | .pytest_cache/ 71 | 72 | # Translations 73 | *.mo 74 | *.pot 75 | 76 | # Django stuff: 77 | *.log 78 | local_settings.py 79 | db.sqlite3 80 | 81 | # Flask stuff: 82 | instance/ 83 | .webassets-cache 84 | 85 | # Scrapy stuff: 86 | .scrapy 87 | 88 | # Sphinx documentation 89 | docs/_build/ 90 | 91 | # PyBuilder 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | .python-version 103 | 104 | # celery beat schedule file 105 | celerybeat-schedule 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | 138 | ### Project-specific config 139 | 140 | # Log files 141 | *.log 142 | 143 | # Coverage file 144 | *.cov 145 | 146 | # Misc 147 | .envrc 148 | -dist/ 149 | tmp/ 150 | -/config.yaml 151 | -AUTHORS 152 | -.vscode 153 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - id: git-ghost 6 | binary: git-ghost 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - darwin 12 | goarch: 13 | - amd64 14 | - arm64 15 | ldflags: 16 | - -s -w -X github.com/pfnet-research/git-ghost/cmd.Version={{.Version}} -X github.com/pfnet-research/git-ghost/cmd.Revision={{.ShortCommit}} 17 | 18 | dockers: 19 | - image_templates: ["ghcr.io/pfnet-research/{{ .ProjectName }}:{{ .Version }}-amd64"] 20 | dockerfile: release.Dockerfile 21 | goarch: amd64 22 | use: buildx 23 | build_flag_templates: 24 | - --platform=linux/amd64 25 | - --label=org.opencontainers.image.title={{ .ProjectName }} 26 | - --label=org.opencontainers.image.description={{ .ProjectName }} 27 | - --label=org.opencontainers.image.url=https://github.com/pfnet-research/{{ .ProjectName }} 28 | - --label=org.opencontainers.image.source=https://github.com/pfnet-research/{{ .ProjectName }} 29 | - --label=org.opencontainers.image.version={{ .Version }} 30 | - --label=org.opencontainers.image.revision={{ .ShortCommit }} 31 | - --label=org.opencontainers.image.licenses=Apache-2.0 32 | - image_templates: ["ghcr.io/pfnet-research/{{ .ProjectName }}:{{ .Version }}-arm64v8"] 33 | dockerfile: release.Dockerfile 34 | goarch: arm64 35 | use: buildx 36 | build_flag_templates: 37 | - --platform=linux/arm64/v8 38 | - --label=org.opencontainers.image.title={{ .ProjectName }} 39 | - --label=org.opencontainers.image.description={{ .ProjectName }} 40 | - --label=org.opencontainers.image.url=https://github.com/pfnet-research/{{ .ProjectName }} 41 | - --label=org.opencontainers.image.source=https://github.com/pfnet-research/{{ .ProjectName }} 42 | - --label=org.opencontainers.image.version={{ .Version }} 43 | - --label=org.opencontainers.image.revision={{ .ShortCommit }} 44 | - --label=org.opencontainers.image.licenses=Apache-2.0 45 | 46 | docker_manifests: 47 | - name_template: ghcr.io/pfnet-research/{{ .ProjectName }}:{{ .Version }} 48 | image_templates: 49 | - ghcr.io/pfnet-research/{{ .ProjectName }}:{{ .Version }}-amd64 50 | - ghcr.io/pfnet-research/{{ .ProjectName }}:{{ .Version }}-arm64v8 51 | - name_template: ghcr.io/pfnet-research/{{ .ProjectName }}:latest 52 | image_templates: 53 | - ghcr.io/pfnet-research/{{ .ProjectName }}:{{ .Version }}-amd64 54 | - ghcr.io/pfnet-research/{{ .ProjectName }}:{{ .Version }}-arm64v8 55 | 56 | release: 57 | prerelease: auto 58 | -------------------------------------------------------------------------------- /.tagpr: -------------------------------------------------------------------------------- 1 | # config file for the tagpr in git config format 2 | # The tagpr generates the initial configuration, which you can rewrite to suit your environment. 3 | # CONFIGURATIONS: 4 | # tagpr.releaseBranch 5 | # Generally, it is "main." It is the branch for releases. The pcpr tracks this branch, 6 | # creates or updates a pull request as a release candidate, or tags when they are merged. 7 | # 8 | # tagpr.versionFile 9 | # Versioning file containing the semantic version needed to be updated at release. 10 | # It will be synchronized with the "git tag". 11 | # Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc. 12 | # Sometimes the source code file, such as version.go or Bar.pm, is used. 13 | # If you do not want to use versioning files but only git tags, specify the "-" string here. 14 | # You can specify multiple version files by comma separated strings. 15 | # 16 | # tagpr.vPrefix 17 | # Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true) 18 | # This is only a tagging convention, not how it is described in the version file. 19 | # 20 | # tagpr.changelog (Optional) 21 | # Flag whether or not changelog is added or changed during the release. 22 | # 23 | # tagpr.command (Optional) 24 | # Command to change files just before release. 25 | # 26 | # tagpr.tmplate (Optional) 27 | # Pull request template in go template format 28 | # 29 | # tagpr.release (Optional) 30 | # GitHub Release creation behavior after tagging [true, draft, false] 31 | # If this value is not set, the release is to be created. 32 | [tagpr] 33 | vPrefix = true 34 | releaseBranch = master 35 | versionFile = - 36 | changelog = false 37 | -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | ## Third Party Dependencies & Licenses 4 | 5 | | Name | License | 6 | | -------------------------------------------------------------------------------- | ---------- | 7 | | [github.com/spf13/cobra](https://github.com/spf13/cobra) | Apache-2.0 | 8 | | [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) | MIT | 9 | | [github.com/stretchr/testify](https://github.com/stretchr/testify) | MIT | 10 | | [github.com/hashicorp/go-multierror](https://github.com/hashicorp/go-multierror) | MPL-2.0 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := git-ghost 2 | PROJECTROOT := $(shell pwd) 3 | REVISION := $(shell git rev-parse --short HEAD) 4 | OUTDIR ?= $(PROJECTROOT)/dist 5 | RELEASE_TAG ?= 6 | GITHUB_USER := pfnet-research 7 | GITHUB_REPO := git-ghost 8 | GITHUB_REPO_URL := git@github.com:pfnet-research/git-ghost.git 9 | GITHUB_TOKEN ?= 10 | 11 | LDFLAGS := -ldflags="-s -w -X \"github.com/pfnet-research/git-ghost/cmd.Revision=$(REVISION)\" -extldflags \"-static\"" 12 | 13 | .PHONY: build 14 | build: 15 | go build -tags netgo -installsuffix netgo $(LDFLAGS) -o $(OUTDIR)/$(NAME) 16 | 17 | .PHONY: install 18 | install: 19 | go install -tags netgo -installsuffix netgo $(LDFLAGS) 20 | 21 | .PHONY: build-linux-amd64 22 | build-linux-amd64: 23 | make build \ 24 | GOOS=linux \ 25 | GOARCH=amd64 \ 26 | NAME=git-ghost-linux-amd64 27 | 28 | .PHONY: build-linux 29 | build-linux: build-linux-amd64 30 | 31 | .PHONY: build-darwin 32 | build-darwin: 33 | make build \ 34 | GOOS=darwin \ 35 | NAME=git-ghost-darwin-amd64 36 | 37 | .PHONY: build-windows 38 | build-windows: 39 | make build \ 40 | GOARCH=amd64 \ 41 | GOOS=windows \ 42 | NAME=git-ghost-windows-amd64.exe 43 | 44 | .PHONY: build-all 45 | build-all: build-linux build-darwin build-windows 46 | 47 | .PHONY: lint 48 | lint: 49 | golangci-lint run --config golangci.yml 50 | 51 | .PHONY: e2e 52 | e2e: 53 | @go test -v $(PROJECTROOT)/test/e2e/e2e_test.go 54 | 55 | .PHONY: update-license 56 | update-license: 57 | @python3 ./scripts/license/add.py -v 58 | 59 | .PHONY: check-license 60 | check-license: 61 | @python3 ./scripts/license/check.py -v 62 | 63 | .PHONY: coverage 64 | coverage: 65 | @go test -tags no_e2e -covermode=count -coverprofile=profile.cov -coverpkg ./pkg/...,./cmd/... $(shell go list ./... | grep -v /vendor/) 66 | @go tool cover -func=profile.cov 67 | 68 | .PHONY: clean 69 | clean: 70 | rm -rf $(OUTDIR)/* 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-ghost 2 | 3 | [![GoDoc][godoc-image]][godoc-link] 4 | [![Build Status][build-image]][build-link] 5 | [![Coverage Status][cov-image]][cov-link] 6 | 7 | Git Ghost is a command line tool for synchronizing your working directory efficiently to a remote place without commiting changes. 8 | 9 | ## Concept 10 | 11 | Git Ghost creates 2 types of branches to synchronize your working directory. 12 | 13 | ### Commits Branch 14 | 15 | This type of branch contains commits between 2 commits which can exist only in your working directory. 16 | 17 | ### Diff Branch 18 | 19 | This type of branch contains modifications from a specific commit in your working directory. 20 | 21 | ## Installing 22 | 23 | ### From Source 24 | 25 | Install the binary from source: execute, 26 | 27 | ```bash 28 | $ git clone https://github.com/pfnet-research/git-ghost 29 | $ cd git-ghost 30 | $ make install 31 | ``` 32 | 33 | Compiled binary is located in `dist` folder. 34 | 35 | ### Releases 36 | 37 | The binaries of each releases are available in [Releases](../../releases). 38 | 39 | ## Getting Started 40 | 41 | First, create an empty repository which can be accessible from a remote place. Set the URL as `GIT_GHOST_REPO` env. 42 | 43 | Assume your have a local working directory `DIR_L` and a remote directory to be synchronized `DIR_R`. 44 | 45 | ## Case 1 (`DIR_L` HEAD == `DIR_R` HEAD) 46 | 47 | You can synchoronize local modifications. 48 | 49 | ```bash 50 | $ cd 51 | $ git-ghost push 52 | 53 | $ git-ghost show 54 | ... 55 | $ cd 56 | $ git-ghost pull 57 | ``` 58 | 59 | ## Case 2 (`DIR_L` HEAD \> `DIR_R` HEAD) 60 | 61 | You can synchronize local commits and modifications. 62 | 63 | Assume `DIR_R`'s HEAD is HASH\_R. 64 | 65 | ```bash 66 | $ cd 67 | $ git-ghost push all 68 | 69 | 70 | $ git-ghost show all 71 | ... 72 | $ cd 73 | $ git-ghost pull all 74 | ``` 75 | 76 | ## Development 77 | 78 | ``` 79 | # checkout this repo to $GOPATH/src/git-ghost 80 | $ cd $GOPATH/src 81 | $ git clone git@github.com:pfnet-research/git-ghost.git 82 | $ cd git-ghost 83 | 84 | # build 85 | $ make build 86 | 87 | # see godoc 88 | $ go get golang.org/x/tools/cmd/godoc 89 | $ godoc -http=:6060 # access http://localhost:6060 in browser 90 | ``` 91 | 92 | ## Contributing 93 | 94 | 1. Fork it 95 | 2. Create your feature branch (`git checkout -b my-new-feature`) 96 | 3. Commit your changes (`git commit -am 'Add some feature'`) 97 | 4. Push to the branch (`git push origin my-new-feature`) 98 | 5. Create new [Pull Request](../../pull/new/master) 99 | 100 | ## Release 101 | 102 | Release flow is fully automated. All the maintainer need to do is just to approve and merge [the next release PR](https://github.com/pfnet-research/git-ghost/pulls?q=is%3Apr+is%3Aopen+label%3Atagpr+) 103 | 104 | ## Copyright 105 | 106 | Copyright (c) 2019 Preferred Networks. See [LICENSE](LICENSE) for details. 107 | 108 | [build-image]: https://github.com/pfnet-research/git-ghost/actions/workflows/ci.yml/badge.svg 109 | [build-link]: https://github.com/pfnet-research/git-ghost/actions/workflows/ci.yml 110 | [cov-image]: https://coveralls.io/repos/github/pfnet-research/git-ghost/badge.svg?branch=master 111 | [cov-link]: https://coveralls.io/github/pfnet-research/git-ghost?branch=master 112 | [godoc-image]: https://godoc.org/github.com/pfnet-research/git-ghost?status.svg 113 | [godoc-link]: https://godoc.org/github.com/pfnet-research/git-ghost 114 | -------------------------------------------------------------------------------- /SPEC.md: -------------------------------------------------------------------------------- 1 | # Spec 2 | ## Ghost Repo 3 | The ghost creates a patch for your uncommitted modifications with the following spec into a ghost repo. 4 | __Word Definitions in this section__ 5 | | name | description | example | 6 | |--------|--------|--------| 7 | | `GHOST_BRANCH_PREFIX` | A prefix to identify branches for ghost. | `git-ghost` | 8 | | `REMOTE_BASE_COMMIT` | A full base commit hash of a source repo. It is supposed to exist in a remote source repo when the ghost apply patches. | `f2e8fbf0f2c1527ad208faa5d08d4b377ce962a3` | 9 | | `LOCAL_BASE_COMMIT` | A full base commit hash of a source repo to make a local modifications diff. | `1fd368bbe90a2e9079d86fa63398a7cd06a79577` | 10 | | `LOCAL_MOD_HASH` | A content hash of local modifications diff. | `35d5d6474c92a780c6be4679cee3a72cd1bdfe99` | 11 | ### Branch Structure 12 | The ghost creates 2 kinds of branches. 13 | #### Local Base Branch 14 | __Format__: `$GHOST_BRANCH_PREFIX/$REMOTE_BASE_COMMIT..$LOCAL_BASE_COMMIT` 15 | __Directory Structure__ 16 | ``` 17 | / 18 | └─ commits.bundle 19 | ``` 20 | `commits.patch` is a diff bundle from a remote base commit to a local base commit. It is not created if a remote base commit equals to a local base commit. 21 | The file is created by the following command. 22 | ``` 23 | $ git format-patch --binary --stdout $REMOTE_BASE_COMMIT..$LOCAL_BASE_COMMIT > commits.patch 24 | ``` 25 | And it can be applied by the following command. 26 | ``` 27 | $ git pull --ff-only --no-tags commits.patch $GHOST_BRANCH_PREFIX/$REMOTE_BASE_COMMIT..$LOCAL_BASE_COMMIT 28 | ``` 29 | #### Local Mod Branch 30 | __Format__: `$GHOST_BRANCH_PREFIX/$LOCAL_BASE_COMMIT/$LOCAL_MOD_HASH` 31 | __Directory Structure__ 32 | ``` 33 | / 34 | └─ patch.diff 35 | ``` 36 | `patch.diff` is a patch file of uncommitted local modifications from a local base commit. 37 | The file is created by the following command. 38 | ``` 39 | $ git diff --binary $LOCAL_BASE_COMMIT > local-mod.patch 40 | ``` 41 | And it can be applied by the following command. 42 | ``` 43 | $ git apply local-mod.patch 44 | ``` 45 | -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "log" 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | const ( 27 | bashCompletionFunc = ` 28 | # Need _git_ghost function to autocomplete on 'git ghost' instead of 'git-ghost' 29 | _git_ghost () 30 | { 31 | __start_git-ghost 32 | } 33 | __git-ghost_get_hash() { 34 | local ghost_out 35 | # TODO: Support second and third argument completion 36 | if ghost_out=$(git-ghost list -o only-from --no-headers --from "$1*" | uniq 2>/dev/null); then 37 | __git-ghost_debug "${FUNCNAME[0]}: ${ghost_out} -- $cur" 38 | COMPREPLY+=( $( compgen -W "${ghost_out[*]}" -- "$cur" ) ) 39 | fi 40 | } 41 | __git-ghost_custom_func() { 42 | case ${last_command} in 43 | git-ghost_push_diff | git-ghost_push_commits | git-ghost_push_all | \ 44 | git-ghost_pull_diff | git-ghost_pull_commits | git-ghost_pull_all | \ 45 | git-ghost_show_diff | git-ghost_show_commits | git-ghost_show_all ) 46 | __git-ghost_get_hash 47 | return 48 | ;; 49 | git-ghost_list_diff | git-ghost_list_commits | git-ghost_list_all | \ 50 | git-ghost_delete_diff | git-ghost_delete_commits | git-ghost_delete_all ) 51 | # TODO: Support --from and --to completion 52 | return 53 | ;; 54 | *) 55 | ;; 56 | esac 57 | } 58 | ` 59 | ) 60 | 61 | func init() { 62 | RootCmd.AddCommand(completionCmd) 63 | } 64 | 65 | var completionCmd = &cobra.Command{ 66 | Use: "completion SHELL", 67 | Short: "output shell completion code for the specified shell (bash or zsh)", 68 | Long: `Write bash or zsh shell completion code to standard output. 69 | 70 | For bash, ensure you have bash completions installed and enabled. 71 | To access completions in your current shell, run 72 | $ source <(git-ghost completion bash) 73 | Alternatively, write it to a file and source in .bash_profile 74 | 75 | For zsh, output to a file in a directory referenced by the $fpath shell 76 | variable. 77 | `, 78 | Args: cobra.ExactArgs(1), 79 | Run: func(cmd *cobra.Command, args []string) { 80 | shell := args[0] 81 | RootCmd.BashCompletionFunction = bashCompletionFunc 82 | availableCompletions := map[string]func(io.Writer) error{ 83 | "bash": RootCmd.GenBashCompletion, 84 | "zsh": RootCmd.GenZshCompletion, 85 | } 86 | completion, ok := availableCompletions[shell] 87 | if !ok { 88 | fmt.Printf("Invalid shell '%s'. The supported shells are bash and zsh.\n", shell) 89 | os.Exit(1) 90 | } 91 | if err := completion(os.Stdout); err != nil { 92 | log.Fatal(err) 93 | } 94 | }, 95 | } 96 | -------------------------------------------------------------------------------- /cmd/delete.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/pfnet-research/git-ghost/pkg/ghost" 22 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 23 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 24 | 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | func init() { 29 | RootCmd.AddCommand(NewDeleteCommand()) 30 | } 31 | 32 | type deleteFlags struct { 33 | hashFrom string 34 | hashTo string 35 | all bool 36 | dryrun bool 37 | } 38 | 39 | func NewDeleteCommand() *cobra.Command { 40 | var ( 41 | deleteFlags deleteFlags 42 | ) 43 | 44 | var command = &cobra.Command{ 45 | Use: "delete", 46 | Short: "delete ghost branches of diffs.", 47 | Long: "delete ghost branches of diffs.", 48 | Args: cobra.NoArgs, 49 | Run: runDeleteDiffCommand(&deleteFlags), 50 | } 51 | command.AddCommand(&cobra.Command{ 52 | Use: "commits", 53 | Short: "delete ghost branches of commits.", 54 | Long: "delete ghost branches of commits.", 55 | Args: cobra.NoArgs, 56 | Run: runDeleteCommitsCommand(&deleteFlags), 57 | }) 58 | command.AddCommand(&cobra.Command{ 59 | Use: "diff", 60 | Short: "delete ghost branches of diffs.", 61 | Long: "delete ghost branches of diffs.", 62 | Args: cobra.NoArgs, 63 | Run: runDeleteDiffCommand(&deleteFlags), 64 | }) 65 | command.AddCommand(&cobra.Command{ 66 | Use: "all", 67 | Short: "delete ghost branches of all types.", 68 | Long: "delete ghost branches of all types.", 69 | Args: cobra.NoArgs, 70 | Run: runDeleteAllCommand(&deleteFlags), 71 | }) 72 | command.PersistentFlags().StringVar(&deleteFlags.hashFrom, "from", "", "commit or diff hash to which ghost branches are deleted.") 73 | command.PersistentFlags().StringVar(&deleteFlags.hashTo, "to", "", "commit or diff hash from which ghost branches are deleted.") 74 | command.PersistentFlags().BoolVar(&deleteFlags.all, "all", false, "flag to ensure multiple ghost branches.") 75 | command.PersistentFlags().BoolVar(&deleteFlags.dryrun, "dry-run", false, "If true, only print the branch names that would be deleted, without deleting them.") 76 | return command 77 | } 78 | 79 | func runDeleteCommitsCommand(flags *deleteFlags) func(cmd *cobra.Command, args []string) { 80 | return func(cmd *cobra.Command, args []string) { 81 | err := flags.validate() 82 | if err != nil { 83 | errors.LogErrorWithStack(err) 84 | os.Exit(1) 85 | } 86 | opts := ghost.DeleteOptions{ 87 | WorkingEnvSpec: types.WorkingEnvSpec{ 88 | SrcDir: globalOpts.srcDir, 89 | GhostWorkingDir: globalOpts.ghostWorkDir, 90 | GhostRepo: globalOpts.ghostRepo, 91 | }, 92 | ListCommitsBranchSpec: &types.ListCommitsBranchSpec{ 93 | Prefix: globalOpts.ghostPrefix, 94 | HashFrom: flags.hashFrom, 95 | HashTo: flags.hashTo, 96 | }, 97 | Dryrun: flags.dryrun, 98 | } 99 | 100 | res, err := ghost.Delete(opts) 101 | if err != nil { 102 | errors.LogErrorWithStack(err) 103 | os.Exit(1) 104 | } 105 | fmt.Print(res.PrettyString()) 106 | } 107 | } 108 | 109 | func runDeleteDiffCommand(flags *deleteFlags) func(cmd *cobra.Command, args []string) { 110 | return func(cmd *cobra.Command, args []string) { 111 | err := flags.validate() 112 | if err != nil { 113 | errors.LogErrorWithStack(err) 114 | os.Exit(1) 115 | } 116 | opts := ghost.DeleteOptions{ 117 | WorkingEnvSpec: types.WorkingEnvSpec{ 118 | SrcDir: globalOpts.srcDir, 119 | GhostWorkingDir: globalOpts.ghostWorkDir, 120 | GhostRepo: globalOpts.ghostRepo, 121 | }, 122 | ListDiffBranchSpec: &types.ListDiffBranchSpec{ 123 | Prefix: globalOpts.ghostPrefix, 124 | HashFrom: flags.hashFrom, 125 | HashTo: flags.hashTo, 126 | }, 127 | Dryrun: flags.dryrun, 128 | } 129 | 130 | res, err := ghost.Delete(opts) 131 | if err != nil { 132 | errors.LogErrorWithStack(err) 133 | os.Exit(1) 134 | } 135 | fmt.Print(res.PrettyString()) 136 | } 137 | } 138 | 139 | func runDeleteAllCommand(flags *deleteFlags) func(cmd *cobra.Command, args []string) { 140 | return func(cmd *cobra.Command, args []string) { 141 | err := flags.validate() 142 | if err != nil { 143 | errors.LogErrorWithStack(err) 144 | os.Exit(1) 145 | } 146 | opts := ghost.DeleteOptions{ 147 | WorkingEnvSpec: types.WorkingEnvSpec{ 148 | SrcDir: globalOpts.srcDir, 149 | GhostWorkingDir: globalOpts.ghostWorkDir, 150 | GhostRepo: globalOpts.ghostRepo, 151 | }, 152 | ListCommitsBranchSpec: &types.ListCommitsBranchSpec{ 153 | Prefix: globalOpts.ghostPrefix, 154 | HashFrom: flags.hashFrom, 155 | HashTo: flags.hashTo, 156 | }, 157 | ListDiffBranchSpec: &types.ListDiffBranchSpec{ 158 | Prefix: globalOpts.ghostPrefix, 159 | HashFrom: flags.hashFrom, 160 | HashTo: flags.hashTo, 161 | }, 162 | Dryrun: flags.dryrun, 163 | } 164 | 165 | res, err := ghost.Delete(opts) 166 | if err != nil { 167 | errors.LogErrorWithStack(err) 168 | os.Exit(1) 169 | } 170 | fmt.Print(res.PrettyString()) 171 | } 172 | } 173 | 174 | func (flags deleteFlags) validate() errors.GitGhostError { 175 | if (flags.hashFrom == "" || flags.hashTo == "") && !flags.all && !flags.dryrun { 176 | return errors.Errorf("all must be set if multiple ghosts branches are deleted") 177 | } 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /cmd/gc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | func init() { 24 | RootCmd.AddCommand(gcCmd) 25 | } 26 | 27 | var gcCmd = &cobra.Command{ 28 | Use: "gc", 29 | Short: "gc ghost commits from remote repository.", 30 | Long: "gc ghost commits from remote repository.", 31 | Run: func(cmd *cobra.Command, args []string) { 32 | fmt.Println("gc command") 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "regexp" 21 | "strings" 22 | 23 | "github.com/pfnet-research/git-ghost/pkg/ghost" 24 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 25 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | func init() { 31 | RootCmd.AddCommand(NewListCommand()) 32 | } 33 | 34 | var outputTypes = []string{"only-from", "only-to"} 35 | var regexpOutputPattern = regexp.MustCompile("^(|" + strings.Join(outputTypes, "|") + ")$") 36 | 37 | type listFlags struct { 38 | hashFrom string 39 | hashTo string 40 | noHeaders bool 41 | output string 42 | } 43 | 44 | func NewListCommand() *cobra.Command { 45 | var ( 46 | listFlags listFlags 47 | ) 48 | 49 | var command = &cobra.Command{ 50 | Use: "list", 51 | Short: "list ghost branches of diffs.", 52 | Long: "list ghost branches of diffs.", 53 | Args: cobra.NoArgs, 54 | Run: runListDiffCommand(&listFlags), 55 | } 56 | command.AddCommand(&cobra.Command{ 57 | Use: "commits", 58 | Short: "list ghost branches of commits.", 59 | Long: "list ghost branches of commits.", 60 | Args: cobra.NoArgs, 61 | Run: runListCommitsCommand(&listFlags), 62 | }) 63 | command.AddCommand(&cobra.Command{ 64 | Use: "diff", 65 | Short: "list ghost branches of diffs.", 66 | Long: "list ghost branches of diffs.", 67 | Args: cobra.NoArgs, 68 | Run: runListDiffCommand(&listFlags), 69 | }) 70 | command.AddCommand(&cobra.Command{ 71 | Use: "all", 72 | Short: "list ghost branches of all types.", 73 | Long: "list ghost branches of all types.", 74 | Args: cobra.NoArgs, 75 | Run: runListAllCommand(&listFlags), 76 | }) 77 | command.PersistentFlags().StringVar(&listFlags.hashFrom, "from", "", "commit or diff hash to which ghost branches are listed.") 78 | command.PersistentFlags().StringVar(&listFlags.hashTo, "to", "", "commit or diff hash from which ghost branches are listed.") 79 | command.PersistentFlags().BoolVar(&listFlags.noHeaders, "no-headers", false, "When using the default, only-from or only-to output format, don't print headers (default print headers).") 80 | command.PersistentFlags().StringVarP(&listFlags.output, "output", "o", "", "Output format. One of: only-from|only-to") 81 | return command 82 | } 83 | 84 | func runListCommitsCommand(flags *listFlags) func(cmd *cobra.Command, args []string) { 85 | return func(cmd *cobra.Command, args []string) { 86 | err := flags.validate() 87 | if err != nil { 88 | errors.LogErrorWithStack(err) 89 | os.Exit(1) 90 | } 91 | opts := ghost.ListOptions{ 92 | WorkingEnvSpec: types.WorkingEnvSpec{ 93 | SrcDir: globalOpts.srcDir, 94 | GhostWorkingDir: globalOpts.ghostWorkDir, 95 | GhostRepo: globalOpts.ghostRepo, 96 | }, 97 | ListCommitsBranchSpec: &types.ListCommitsBranchSpec{ 98 | Prefix: globalOpts.ghostPrefix, 99 | HashFrom: flags.hashFrom, 100 | HashTo: flags.hashTo, 101 | }, 102 | } 103 | 104 | res, err := ghost.List(opts) 105 | if err != nil { 106 | errors.LogErrorWithStack(err) 107 | os.Exit(1) 108 | } 109 | fmt.Print(res.PrettyString(!flags.noHeaders, flags.output)) 110 | } 111 | } 112 | 113 | func runListDiffCommand(flags *listFlags) func(cmd *cobra.Command, args []string) { 114 | return func(cmd *cobra.Command, args []string) { 115 | err := flags.validate() 116 | if err != nil { 117 | errors.LogErrorWithStack(err) 118 | os.Exit(1) 119 | } 120 | opts := ghost.ListOptions{ 121 | WorkingEnvSpec: types.WorkingEnvSpec{ 122 | SrcDir: globalOpts.srcDir, 123 | GhostWorkingDir: globalOpts.ghostWorkDir, 124 | GhostRepo: globalOpts.ghostRepo, 125 | }, 126 | ListDiffBranchSpec: &types.ListDiffBranchSpec{ 127 | Prefix: globalOpts.ghostPrefix, 128 | HashFrom: flags.hashFrom, 129 | HashTo: flags.hashTo, 130 | }, 131 | } 132 | 133 | res, err := ghost.List(opts) 134 | if err != nil { 135 | errors.LogErrorWithStack(err) 136 | os.Exit(1) 137 | } 138 | fmt.Print(res.PrettyString(!flags.noHeaders, flags.output)) 139 | } 140 | } 141 | 142 | func runListAllCommand(flags *listFlags) func(cmd *cobra.Command, args []string) { 143 | return func(cmd *cobra.Command, args []string) { 144 | err := flags.validate() 145 | if err != nil { 146 | errors.LogErrorWithStack(err) 147 | os.Exit(1) 148 | } 149 | opts := ghost.ListOptions{ 150 | WorkingEnvSpec: types.WorkingEnvSpec{ 151 | SrcDir: globalOpts.srcDir, 152 | GhostWorkingDir: globalOpts.ghostWorkDir, 153 | GhostRepo: globalOpts.ghostRepo, 154 | }, 155 | ListCommitsBranchSpec: &types.ListCommitsBranchSpec{ 156 | Prefix: globalOpts.ghostPrefix, 157 | HashFrom: flags.hashFrom, 158 | HashTo: flags.hashTo, 159 | }, 160 | ListDiffBranchSpec: &types.ListDiffBranchSpec{ 161 | Prefix: globalOpts.ghostPrefix, 162 | HashFrom: flags.hashFrom, 163 | HashTo: flags.hashTo, 164 | }, 165 | } 166 | 167 | res, err := ghost.List(opts) 168 | if err != nil { 169 | errors.LogErrorWithStack(err) 170 | os.Exit(1) 171 | } 172 | fmt.Print(res.PrettyString(!flags.noHeaders, flags.output)) 173 | } 174 | } 175 | 176 | func (flags listFlags) validate() errors.GitGhostError { 177 | if !regexpOutputPattern.MatchString(flags.output) { 178 | return errors.Errorf("output must be one of %v", outputTypes) 179 | } 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /cmd/pull.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/pfnet-research/git-ghost/pkg/ghost" 21 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 22 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 23 | 24 | log "github.com/sirupsen/logrus" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | func init() { 29 | RootCmd.AddCommand(NewPullCommand()) 30 | } 31 | 32 | type pullFlags struct { 33 | // forceApply bool 34 | } 35 | 36 | func NewPullCommand() *cobra.Command { 37 | var ( 38 | flags pullFlags 39 | ) 40 | command := &cobra.Command{ 41 | Use: "pull [from-hash(default=HEAD)] [diff-hash]", 42 | Short: "pull commits(hash1...hash2), diff(hash...current state) from ghost repo and apply them to working dir", 43 | Long: "pull commits or diff or all from ghost repo and apply them to working dir. If you didn't specify any subcommand, this commands works as an alias for 'pull diff' command.", 44 | Args: cobra.RangeArgs(1, 2), 45 | Run: runPullDiffCommand(&flags), 46 | } 47 | // command.PersistentFlags().BoolVarP(&flags.forceApply, "force", "f", true, "force apply pulled ghost branches to working dir") 48 | 49 | command.AddCommand(&cobra.Command{ 50 | Use: "diff [diff-from-hash(default=HEAD)] [diff-hash]", 51 | Short: "pull diff from ghost repo and apply it to working dir", 52 | Long: "pull diff from [diff-from-hash] to [diff-hash] from your ghost repo and apply it to working dir", 53 | Args: cobra.RangeArgs(1, 2), 54 | Run: runPullDiffCommand(&flags), 55 | }) 56 | command.AddCommand(&cobra.Command{ 57 | Use: "commits [from-hash(default=HEAD)] [to-hash]", 58 | Short: "pull commits from ghost repo and apply it to working dir", 59 | Long: "pull commits from [from-hash] to [to-hash] from your ghost repo and apply it to working dir", 60 | Args: cobra.RangeArgs(1, 2), 61 | Run: runPullCommitsCommand(&flags), 62 | }) 63 | command.AddCommand(&cobra.Command{ 64 | Use: "all [from-hash(default=HEAD)] [to-hash] [diff-hash]", 65 | Short: "pull both commits and diff from ghost repo and apply them to working dir sequentially", 66 | Long: "pull commits([from-hash]...[to-hash]) and diff([to-hash]...[diff-hash]) and apply them to working dir sequentially", 67 | Args: cobra.RangeArgs(2, 3), 68 | Run: runPullAllCommand(&flags), 69 | }) 70 | return command 71 | } 72 | 73 | type pullCommitsArg struct { 74 | commitsFrom string 75 | commitsTo string 76 | } 77 | 78 | func newPullCommitsArg(args []string) pullCommitsArg { 79 | arg := pullCommitsArg{ 80 | commitsFrom: "HEAD", 81 | commitsTo: "", 82 | } 83 | 84 | if len(args) >= 2 { 85 | arg.commitsFrom = args[0] 86 | arg.commitsTo = args[1] 87 | return arg 88 | } 89 | 90 | if len(args) >= 1 { 91 | arg.commitsTo = args[0] 92 | return arg 93 | } 94 | 95 | return arg 96 | } 97 | 98 | func (arg pullCommitsArg) validate() errors.GitGhostError { 99 | if err := nonEmpty("commit-from", arg.commitsFrom); err != nil { 100 | return err 101 | } 102 | if err := nonEmpty("commit-to", arg.commitsTo); err != nil { 103 | return err 104 | } 105 | return nil 106 | } 107 | 108 | func runPullCommitsCommand(flags *pullFlags) func(cmd *cobra.Command, args []string) { 109 | return func(cmd *cobra.Command, args []string) { 110 | arg := newPullCommitsArg(args) 111 | if err := arg.validate(); err != nil { 112 | errors.LogErrorWithStack(err) 113 | os.Exit(1) 114 | } 115 | 116 | options := ghost.PullOptions{ 117 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 118 | CommitsBranchSpec: &types.CommitsBranchSpec{ 119 | Prefix: globalOpts.ghostPrefix, 120 | CommittishFrom: arg.commitsFrom, 121 | CommittishTo: arg.commitsTo, 122 | }, 123 | // ForceApply: flags.forceApply, 124 | } 125 | 126 | err := ghost.Pull(options) 127 | if err != nil { 128 | errors.LogErrorWithStack(err) 129 | os.Exit(1) 130 | } 131 | } 132 | } 133 | 134 | type pullDiffArg struct { 135 | diffFrom string 136 | diffHash string 137 | } 138 | 139 | func newPullDiffArg(args []string) pullDiffArg { 140 | arg := pullDiffArg{ 141 | diffFrom: "HEAD", 142 | diffHash: "", 143 | } 144 | 145 | if len(args) >= 2 { 146 | arg.diffFrom = args[0] 147 | arg.diffHash = args[1] 148 | return arg 149 | } 150 | 151 | if len(args) >= 1 { 152 | arg.diffHash = args[0] 153 | return arg 154 | } 155 | 156 | return arg 157 | } 158 | 159 | func (arg pullDiffArg) validate() errors.GitGhostError { 160 | if err := nonEmpty("diff-from-hash", arg.diffFrom); err != nil { 161 | return err 162 | } 163 | if err := nonEmpty("diff-hash", arg.diffHash); err != nil { 164 | return err 165 | } 166 | return nil 167 | } 168 | 169 | func runPullDiffCommand(flags *pullFlags) func(cmd *cobra.Command, args []string) { 170 | return func(cmd *cobra.Command, args []string) { 171 | arg := newPullDiffArg(args) 172 | if err := arg.validate(); err != nil { 173 | errors.LogErrorWithStack(err) 174 | os.Exit(1) 175 | } 176 | 177 | options := ghost.PullOptions{ 178 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 179 | PullableDiffBranchSpec: &types.PullableDiffBranchSpec{ 180 | Prefix: globalOpts.ghostPrefix, 181 | CommittishFrom: arg.diffFrom, 182 | DiffHash: arg.diffHash, 183 | }, 184 | // ForceApply: flags.forceApply, 185 | } 186 | 187 | err := ghost.Pull(options) 188 | if err != nil { 189 | errors.LogErrorWithStack(err) 190 | os.Exit(1) 191 | } 192 | } 193 | } 194 | 195 | func runPullAllCommand(flags *pullFlags) func(cmd *cobra.Command, args []string) { 196 | return func(cmd *cobra.Command, args []string) { 197 | var pullCommitsArg pullCommitsArg 198 | var pullDiffArg pullDiffArg 199 | 200 | switch len(args) { 201 | case 3: 202 | pullCommitsArg = newPullCommitsArg(args[0:2]) 203 | pullDiffArg = newPullDiffArg(args[1:]) 204 | case 2: 205 | pullCommitsArg = newPullCommitsArg(args[0:1]) 206 | pullDiffArg = newPullDiffArg(args) 207 | default: 208 | log.Error(cmd.Args(cmd, args)) 209 | os.Exit(1) 210 | } 211 | 212 | if err := pullCommitsArg.validate(); err != nil { 213 | errors.LogErrorWithStack(err) 214 | os.Exit(1) 215 | } 216 | if err := pullDiffArg.validate(); err != nil { 217 | errors.LogErrorWithStack(err) 218 | os.Exit(1) 219 | } 220 | 221 | options := ghost.PullOptions{ 222 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 223 | CommitsBranchSpec: &types.CommitsBranchSpec{ 224 | Prefix: globalOpts.ghostPrefix, 225 | CommittishFrom: pullCommitsArg.commitsFrom, 226 | CommittishTo: pullCommitsArg.commitsTo, 227 | }, 228 | PullableDiffBranchSpec: &types.PullableDiffBranchSpec{ 229 | Prefix: globalOpts.ghostPrefix, 230 | CommittishFrom: pullDiffArg.diffFrom, 231 | DiffHash: pullDiffArg.diffHash, 232 | }, 233 | // ForceApply: flags.forceApply, 234 | } 235 | 236 | err := ghost.Pull(options) 237 | if err != nil { 238 | errors.LogErrorWithStack(err) 239 | os.Exit(1) 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /cmd/push.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/pfnet-research/git-ghost/pkg/ghost" 22 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 23 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 24 | 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | type pushFlags struct { 29 | includedFilepaths []string 30 | followSymlinks bool 31 | } 32 | 33 | func init() { 34 | RootCmd.AddCommand(NewPushCommand()) 35 | } 36 | 37 | func NewPushCommand() *cobra.Command { 38 | var ( 39 | flags pushFlags 40 | ) 41 | command := &cobra.Command{ 42 | Use: "push [from-hash(default=HEAD)]", 43 | Short: "push commits(hash1...hash2), diff(hash...current state) to your ghost repo", 44 | Long: "push commits or diff or all to your ghost repo. If you didn't specify any subcommand, this commands works as an alias for 'push diff' command.", 45 | Args: cobra.RangeArgs(0, 1), 46 | Run: runPushDiffCommand(&flags), 47 | } 48 | command.AddCommand(&cobra.Command{ 49 | Use: "commits [from-hash] [to-hash(default=HEAD)]", 50 | Short: "push commits between two commits to your ghost repo", 51 | Long: "push all the commits between [from-hash]...[to-hash] to your ghost repo.", 52 | Args: cobra.RangeArgs(1, 2), 53 | Run: runPushCommitsCommand(&flags), 54 | }) 55 | command.AddCommand(&cobra.Command{ 56 | Use: "diff [from-hash(default=HEAD)]", 57 | Short: "push diff from a commit to current state of your working dir to your ghost repo", 58 | Long: "push diff from [from-hash] to current state of your working dir to your ghost repo. please be noted that this pushes only diff, which means that it doesn't save any commits information.", 59 | Args: cobra.RangeArgs(0, 1), 60 | Run: runPushDiffCommand(&flags), 61 | }) 62 | command.AddCommand(&cobra.Command{ 63 | Use: "all [commits-from-hash] [diff-from-hash(default=HEAD)]", 64 | Short: "push both commits and diff to your ghost repo", 65 | Long: "push both commits([commits-from-hash]...[diff-from-hash]) and diff([diff-from-hash]...current state) to your ghost repo", 66 | Args: cobra.RangeArgs(1, 2), 67 | Run: runPushAllCommand(&flags), 68 | }) 69 | 70 | command.PersistentFlags().StringSliceVarP(&flags.includedFilepaths, "include", "I", []string{}, "include a non-indexed file, this flag can be repeated to specify multiple files.") 71 | command.PersistentFlags().BoolVar(&flags.followSymlinks, "follow-symlinks", false, "follow symlinks inside the repository.") 72 | 73 | return command 74 | } 75 | 76 | type pushCommitsArg struct { 77 | commitsFrom string 78 | commitsTo string 79 | } 80 | 81 | func newPushCommitsArg(args []string) pushCommitsArg { 82 | pushCommitsArg := pushCommitsArg{ 83 | commitsFrom: "", 84 | commitsTo: "HEAD", 85 | } 86 | if len(args) >= 1 { 87 | pushCommitsArg.commitsFrom = args[0] 88 | } 89 | if len(args) >= 2 { 90 | pushCommitsArg.commitsTo = args[1] 91 | } 92 | return pushCommitsArg 93 | } 94 | 95 | func (arg pushCommitsArg) validate() errors.GitGhostError { 96 | if err := nonEmpty("commit-from", arg.commitsFrom); err != nil { 97 | return err 98 | } 99 | if err := nonEmpty("commit-to", arg.commitsTo); err != nil { 100 | return err 101 | } 102 | if err := isValidCommittish("commit-from", arg.commitsFrom); err != nil { 103 | return err 104 | } 105 | if err := isValidCommittish("commit-to", arg.commitsTo); err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | func runPushCommitsCommand(flags *pushFlags) func(cmd *cobra.Command, args []string) { 112 | return func(cmd *cobra.Command, args []string) { 113 | pushArg := newPushCommitsArg(args) 114 | if err := pushArg.validate(); err != nil { 115 | errors.LogErrorWithStack(err) 116 | os.Exit(1) 117 | } 118 | options := ghost.PushOptions{ 119 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 120 | CommitsBranchSpec: &types.CommitsBranchSpec{ 121 | Prefix: globalOpts.ghostPrefix, 122 | CommittishFrom: pushArg.commitsFrom, 123 | CommittishTo: pushArg.commitsTo, 124 | }, 125 | } 126 | 127 | result, err := ghost.Push(options) 128 | if err != nil { 129 | errors.LogErrorWithStack(err) 130 | os.Exit(1) 131 | } 132 | 133 | if result.CommitsBranch != nil { 134 | fmt.Printf( 135 | "%s %s", 136 | result.CommitsBranch.CommitHashFrom, 137 | result.CommitsBranch.CommitHashTo, 138 | ) 139 | } 140 | } 141 | } 142 | 143 | type pushDiffArg struct { 144 | diffFrom string 145 | } 146 | 147 | func newPushDiffArg(args []string) pushDiffArg { 148 | pushDiffArg := pushDiffArg{ 149 | diffFrom: "HEAD", 150 | } 151 | if len(args) >= 1 { 152 | pushDiffArg.diffFrom = args[0] 153 | } 154 | return pushDiffArg 155 | } 156 | 157 | func (arg pushDiffArg) validate() errors.GitGhostError { 158 | if err := nonEmpty("diff-from", arg.diffFrom); err != nil { 159 | return err 160 | } 161 | if err := isValidCommittish("diff-from", arg.diffFrom); err != nil { 162 | return err 163 | } 164 | return nil 165 | } 166 | 167 | func runPushDiffCommand(flags *pushFlags) func(cmd *cobra.Command, args []string) { 168 | return func(cmd *cobra.Command, args []string) { 169 | pushArg := newPushDiffArg(args) 170 | if err := pushArg.validate(); err != nil { 171 | errors.LogErrorWithStack(err) 172 | os.Exit(1) 173 | } 174 | options := ghost.PushOptions{ 175 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 176 | DiffBranchSpec: &types.DiffBranchSpec{ 177 | Prefix: globalOpts.ghostPrefix, 178 | CommittishFrom: pushArg.diffFrom, 179 | IncludedFilepaths: flags.includedFilepaths, 180 | FollowSymlinks: flags.followSymlinks, 181 | }, 182 | } 183 | 184 | result, err := ghost.Push(options) 185 | if err != nil { 186 | errors.LogErrorWithStack(err) 187 | os.Exit(1) 188 | } 189 | 190 | if result.DiffBranch != nil { 191 | fmt.Printf( 192 | "%s %s", 193 | result.DiffBranch.CommitHashFrom, 194 | result.DiffBranch.DiffHash, 195 | ) 196 | fmt.Print("\n") 197 | } 198 | } 199 | } 200 | 201 | func runPushAllCommand(flags *pushFlags) func(cmd *cobra.Command, args []string) { 202 | return func(cmd *cobra.Command, args []string) { 203 | pushCommitsArg := newPushCommitsArg(args[0:1]) 204 | if err := pushCommitsArg.validate(); err != nil { 205 | errors.LogErrorWithStack(err) 206 | os.Exit(1) 207 | } 208 | 209 | pushDiffArg := newPushDiffArg(args[1:]) 210 | if err := pushDiffArg.validate(); err != nil { 211 | errors.LogErrorWithStack(err) 212 | os.Exit(1) 213 | } 214 | 215 | options := ghost.PushOptions{ 216 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 217 | CommitsBranchSpec: &types.CommitsBranchSpec{ 218 | Prefix: globalOpts.ghostPrefix, 219 | CommittishFrom: pushCommitsArg.commitsFrom, 220 | CommittishTo: pushCommitsArg.commitsTo, 221 | }, 222 | DiffBranchSpec: &types.DiffBranchSpec{ 223 | Prefix: globalOpts.ghostPrefix, 224 | CommittishFrom: pushDiffArg.diffFrom, 225 | IncludedFilepaths: flags.includedFilepaths, 226 | FollowSymlinks: flags.followSymlinks, 227 | }, 228 | } 229 | 230 | result, err := ghost.Push(options) 231 | if err != nil { 232 | errors.LogErrorWithStack(err) 233 | os.Exit(1) 234 | } 235 | 236 | if result.CommitsBranch != nil { 237 | fmt.Printf( 238 | "%s %s", 239 | result.CommitsBranch.CommitHashFrom, 240 | result.CommitsBranch.CommitHashTo, 241 | ) 242 | if result.DiffBranch != nil { 243 | fmt.Print("\n") 244 | } 245 | } 246 | 247 | if result.DiffBranch != nil { 248 | fmt.Printf( 249 | "%s %s", 250 | result.DiffBranch.CommitHashFrom, 251 | result.DiffBranch.DiffHash, 252 | ) 253 | fmt.Print("\n") 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/pfnet-research/git-ghost/pkg/ghost/git" 22 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 23 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 24 | 25 | log "github.com/sirupsen/logrus" 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | type globalFlags struct { 30 | srcDir string 31 | ghostWorkDir string 32 | ghostPrefix string 33 | ghostRepo string 34 | verbose int 35 | } 36 | 37 | func (gf globalFlags) WorkingEnvSpec() types.WorkingEnvSpec { 38 | workingEnvSpec := types.WorkingEnvSpec{ 39 | SrcDir: gf.srcDir, 40 | GhostWorkingDir: gf.ghostWorkDir, 41 | GhostRepo: gf.ghostRepo, 42 | } 43 | userName, userEmail, err := git.GetUserConfig(globalOpts.srcDir) 44 | if err == nil { 45 | workingEnvSpec.GhostUserName = userName 46 | workingEnvSpec.GhostUserEmail = userEmail 47 | } else { 48 | log.Debug("failed to get user name and email of the source directory") 49 | } 50 | return workingEnvSpec 51 | } 52 | 53 | var ( 54 | Version string 55 | Revision string 56 | ) 57 | 58 | var RootCmd = &cobra.Command{ 59 | Use: "git-ghost", 60 | Short: "git-ghost", 61 | SilenceErrors: false, 62 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 63 | if cmd.Use == "version" { 64 | return nil 65 | } 66 | err := validateEnvironment() 67 | if err != nil { 68 | return err 69 | } 70 | err = globalOpts.SetDefaults() 71 | if err != nil { 72 | return err 73 | } 74 | err = globalOpts.Validate() 75 | if err != nil { 76 | return err 77 | } 78 | switch globalOpts.verbose { 79 | case 0: 80 | log.SetLevel(log.ErrorLevel) 81 | case 1: 82 | log.SetLevel(log.InfoLevel) 83 | case 2: 84 | log.SetLevel(log.DebugLevel) 85 | case 3: 86 | log.SetLevel(log.TraceLevel) 87 | default: 88 | log.SetLevel(log.TraceLevel) 89 | } 90 | return nil 91 | }, 92 | } 93 | 94 | var globalOpts globalFlags 95 | 96 | func init() { 97 | cobra.OnInitialize() 98 | RootCmd.PersistentFlags().StringVar(&globalOpts.srcDir, "src-dir", "", "source directory which you create ghost from (default to the current directory)") 99 | RootCmd.PersistentFlags().StringVar(&globalOpts.ghostWorkDir, "ghost-working-dir", "", "local root directory for git-ghost interacting with ghost repository (default to a temporary directory)") 100 | RootCmd.PersistentFlags().StringVar(&globalOpts.ghostPrefix, "ghost-prefix", "", "prefix of ghost branch name (default to GIT_GHOST_PREFIX env, or ghost)") 101 | RootCmd.PersistentFlags().StringVar(&globalOpts.ghostRepo, "ghost-repo", "", "git remote url for ghosts repository (default to GIT_GHOST_REPO env)") 102 | RootCmd.PersistentFlags().CountVarP(&globalOpts.verbose, "verbose", "v", "verbose mode. (1: info, 2: debug, 3: trace)") 103 | RootCmd.AddCommand(versionCmd) 104 | } 105 | 106 | var versionCmd = &cobra.Command{ 107 | Use: "version", 108 | Short: "Print the version number of git-ghost", 109 | Long: `Print the version number of git-ghost`, 110 | Run: func(cmd *cobra.Command, args []string) { 111 | fmt.Printf("git-ghost %s (revision: %s)", Version, Revision) 112 | }, 113 | } 114 | 115 | func validateEnvironment() errors.GitGhostError { 116 | err := git.ValidateGit() 117 | if err != nil { 118 | return errors.New("git is required") 119 | } 120 | return nil 121 | } 122 | 123 | func (flags *globalFlags) SetDefaults() errors.GitGhostError { 124 | if globalOpts.srcDir == "" { 125 | srcDir, err := os.Getwd() 126 | if err != nil { 127 | return errors.New("failed to get the working directory") 128 | } 129 | globalOpts.srcDir = srcDir 130 | } 131 | if globalOpts.ghostWorkDir == "" { 132 | globalOpts.ghostWorkDir = os.TempDir() 133 | } 134 | if globalOpts.ghostPrefix == "" { 135 | ghostPrefixEnv := os.Getenv("GIT_GHOST_PREFIX") 136 | if ghostPrefixEnv == "" { 137 | ghostPrefixEnv = "ghost" 138 | } 139 | globalOpts.ghostPrefix = ghostPrefixEnv 140 | } 141 | if globalOpts.ghostRepo == "" { 142 | globalOpts.ghostRepo = os.Getenv("GIT_GHOST_REPO") 143 | } 144 | return nil 145 | } 146 | 147 | func (flags *globalFlags) Validate() errors.GitGhostError { 148 | if flags.srcDir == "" { 149 | return errors.New("src-dir must be specified") 150 | } 151 | _, err := os.Stat(flags.ghostWorkDir) 152 | if err != nil { 153 | return errors.Errorf("ghost-working-dir is not found (value: %v)", flags.ghostWorkDir) 154 | } 155 | if flags.ghostPrefix == "" { 156 | return errors.New("ghost-prefix must be specified") 157 | } 158 | if flags.ghostRepo == "" { 159 | return errors.New("ghost-repo must be specified") 160 | } 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /cmd/show.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/pfnet-research/git-ghost/pkg/ghost" 21 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 22 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 23 | 24 | log "github.com/sirupsen/logrus" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | func init() { 29 | RootCmd.AddCommand(NewShowCommand()) 30 | } 31 | 32 | func NewShowCommand() *cobra.Command { 33 | command := &cobra.Command{ 34 | Use: "show [from-hash(default=HEAD)] [diff-hash]", 35 | Short: "show commits(hash1...hash2), diff(hash...current state) in ghost repo", 36 | Long: "show commits or diff or all from ghost repo. If you didn't specify any subcommand, this commands works as an alias for 'show diff' command.", 37 | Args: cobra.RangeArgs(1, 2), 38 | Run: runShowDiffCommand, 39 | } 40 | command.AddCommand(&cobra.Command{ 41 | Use: "diff [diff-from-hash(default=HEAD)] [diff-hash]", 42 | Short: "show diff in ghost repo ", 43 | Long: "show diff from [diff-from-hash] to [diff-hash] in ghost repo", 44 | Args: cobra.RangeArgs(1, 2), 45 | Run: runShowDiffCommand, 46 | }) 47 | command.AddCommand(&cobra.Command{ 48 | Use: "commits [from-hash(default=HEAD)] [to-hash]", 49 | Short: "show commits in ghost repo", 50 | Long: "show commits from [from-hash] to [to-hash] in ghost repo", 51 | Args: cobra.RangeArgs(1, 2), 52 | Run: runShowCommitsCommand, 53 | }) 54 | command.AddCommand(&cobra.Command{ 55 | Use: "all [from-hash(default=HEAD)] [to-hash] [diff-hash]", 56 | Short: "show both commits and diff in ghost repo", 57 | Long: "show commits([from-hash]...[to-hash]) and diff([to-hash]...[diff-hash]) in ghost repo", 58 | Args: cobra.RangeArgs(2, 3), 59 | Run: runShowAllCommand, 60 | }) 61 | return command 62 | } 63 | 64 | type showCommitsArg struct { 65 | commitsFrom string 66 | commitsTo string 67 | } 68 | 69 | func newShowCommitsArg(args []string) showCommitsArg { 70 | arg := showCommitsArg{ 71 | commitsFrom: "HEAD", 72 | commitsTo: "", 73 | } 74 | 75 | if len(args) >= 2 { 76 | arg.commitsFrom = args[0] 77 | arg.commitsTo = args[1] 78 | return arg 79 | } 80 | 81 | if len(args) >= 1 { 82 | arg.commitsTo = args[0] 83 | return arg 84 | } 85 | 86 | return arg 87 | } 88 | 89 | func (arg showCommitsArg) validate() errors.GitGhostError { 90 | if err := nonEmpty("commit-from", arg.commitsFrom); err != nil { 91 | return err 92 | } 93 | if err := nonEmpty("commit-to", arg.commitsTo); err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | 99 | func runShowCommitsCommand(cmd *cobra.Command, args []string) { 100 | arg := newShowCommitsArg(args) 101 | if err := arg.validate(); err != nil { 102 | errors.LogErrorWithStack(err) 103 | os.Exit(1) 104 | } 105 | 106 | options := ghost.ShowOptions{ 107 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 108 | CommitsBranchSpec: &types.CommitsBranchSpec{ 109 | Prefix: globalOpts.ghostPrefix, 110 | CommittishFrom: arg.commitsFrom, 111 | CommittishTo: arg.commitsTo, 112 | }, 113 | Writer: os.Stdout, 114 | } 115 | 116 | err := ghost.Show(options) 117 | if err != nil { 118 | errors.LogErrorWithStack(err) 119 | os.Exit(1) 120 | } 121 | } 122 | 123 | type showDiffArg struct { 124 | diffFrom string 125 | diffHash string 126 | } 127 | 128 | func newShowDiffArg(args []string) showDiffArg { 129 | arg := showDiffArg{ 130 | diffFrom: "HEAD", 131 | diffHash: "", 132 | } 133 | 134 | if len(args) >= 2 { 135 | arg.diffFrom = args[0] 136 | arg.diffHash = args[1] 137 | return arg 138 | } 139 | 140 | if len(args) >= 1 { 141 | arg.diffHash = args[0] 142 | return arg 143 | } 144 | 145 | return arg 146 | } 147 | 148 | func (arg showDiffArg) validate() errors.GitGhostError { 149 | if err := nonEmpty("diff-from-hash", arg.diffFrom); err != nil { 150 | return err 151 | } 152 | if err := nonEmpty("diff-hash", arg.diffHash); err != nil { 153 | return err 154 | } 155 | return nil 156 | } 157 | 158 | func runShowDiffCommand(cmd *cobra.Command, args []string) { 159 | arg := newShowDiffArg(args) 160 | if err := arg.validate(); err != nil { 161 | errors.LogErrorWithStack(err) 162 | os.Exit(1) 163 | } 164 | 165 | options := ghost.ShowOptions{ 166 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 167 | PullableDiffBranchSpec: &types.PullableDiffBranchSpec{ 168 | Prefix: globalOpts.ghostPrefix, 169 | CommittishFrom: arg.diffFrom, 170 | DiffHash: arg.diffHash, 171 | }, 172 | Writer: os.Stdout, 173 | } 174 | 175 | err := ghost.Show(options) 176 | if err != nil { 177 | errors.LogErrorWithStack(err) 178 | os.Exit(1) 179 | } 180 | } 181 | 182 | func runShowAllCommand(cmd *cobra.Command, args []string) { 183 | var showCommitsArg showCommitsArg 184 | var showDiffArg showDiffArg 185 | 186 | switch len(args) { 187 | case 3: 188 | showCommitsArg = newShowCommitsArg(args[0:2]) 189 | showDiffArg = newShowDiffArg(args[1:]) 190 | case 2: 191 | showCommitsArg = newShowCommitsArg(args[0:1]) 192 | showDiffArg = newShowDiffArg(args) 193 | default: 194 | log.Error(cmd.Args(cmd, args)) 195 | os.Exit(1) 196 | } 197 | 198 | if err := showCommitsArg.validate(); err != nil { 199 | errors.LogErrorWithStack(err) 200 | os.Exit(1) 201 | } 202 | if err := showDiffArg.validate(); err != nil { 203 | errors.LogErrorWithStack(err) 204 | os.Exit(1) 205 | } 206 | 207 | options := ghost.ShowOptions{ 208 | WorkingEnvSpec: globalOpts.WorkingEnvSpec(), 209 | CommitsBranchSpec: &types.CommitsBranchSpec{ 210 | Prefix: globalOpts.ghostPrefix, 211 | CommittishFrom: showCommitsArg.commitsFrom, 212 | CommittishTo: showCommitsArg.commitsTo, 213 | }, 214 | PullableDiffBranchSpec: &types.PullableDiffBranchSpec{ 215 | Prefix: globalOpts.ghostPrefix, 216 | CommittishFrom: showDiffArg.diffFrom, 217 | DiffHash: showDiffArg.diffHash, 218 | }, 219 | Writer: os.Stdout, 220 | } 221 | 222 | err := ghost.Show(options) 223 | if err != nil { 224 | errors.LogErrorWithStack(err) 225 | os.Exit(1) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /cmd/validation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "github.com/pfnet-research/git-ghost/pkg/ghost/git" 19 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 20 | ) 21 | 22 | func nonEmpty(name, value string) errors.GitGhostError { 23 | if value == "" { 24 | return errors.Errorf("%s must not be empty", name) 25 | } 26 | return nil 27 | } 28 | 29 | func isValidCommittish(name, committish string) errors.GitGhostError { 30 | err := git.ValidateCommittish(globalOpts.srcDir, committish) 31 | if err != nil { 32 | return errors.Errorf("%s is not a valid object", name) 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /examples/argo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN apt-get update -q && apt-get install -yq --no-install-recommends git && \ 4 | apt-get clean && \ 5 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 6 | 7 | COPY . /code/ 8 | WORKDIR /code 9 | -------------------------------------------------------------------------------- /examples/argo/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2019 Preferred Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eu 18 | set -o pipefail 19 | 20 | DIR=$(dirname "$0") 21 | 22 | [[ -z "$GIT_GHOST_REPO" ]] && (echo "GIT_GHOST_REPO must be specified."; exit 1) 23 | [[ -z "$GIT_GHOST_REGISTRY" ]] && (echo "GIT_GHOST_REGISTRY must be specified."; exit 1) 24 | 25 | kubectl get secret git-ghost-git-cred > /dev/null || (cat << EOS 26 | git-ghost-git-cred secret is needed. 27 | You can create it by: 28 | 29 | $ kubectl create secret generic git-ghost-git-cred --from-file=sshkey=\$HOME/.ssh/id_rsa 30 | 31 | EOS 32 | exit 1) 33 | 34 | kubectl get secret git-ghost-docker-cred > /dev/null || (cat << EOS 35 | git-ghost-docker-cred secret is needed. 36 | You can create it by: 37 | 38 | $ kubectl create secret docker-registry git-ghost-docker-cred --docker-username=YOUR_NAME --docker-password=YOUR_PASSWORD 39 | 40 | EOS 41 | exit 1) 42 | 43 | IMAGE_PREFIX= 44 | if [[ -z "$IMAGE_PREFIX" ]]; then 45 | IMAGE_PREFIX=ghcr.io/pfnet-research/ 46 | fi 47 | 48 | IMAGE_TAG= 49 | if [[ -z "$IMAGE_TAG" ]]; then 50 | IMAGE_TAG="latest" 51 | fi 52 | 53 | GIT_REPO=`git remote get-url origin` 54 | git diff --exit-code HEAD > /dev/null && (echo "Make some local modification to try the example."; exit 1) 55 | HASHES=`git ghost push origin/master` 56 | GIT_COMMIT_HASH=`echo $HASHES | cut -f 1 -d " "` 57 | DIFF_HASH=`echo $HASHES | cut -f 2 -d " "` 58 | 59 | argo submit --watch -p image-prefix=$IMAGE_PREFIX -p image-tag=$IMAGE_TAG -p git-repo=$GIT_REPO -p git-ghost-repo=$GIT_GHOST_REPO -p git-ghost-registry=$GIT_GHOST_REGISTRY -p git-commit-hash=$GIT_COMMIT_HASH -p diff-hash=$DIFF_HASH $DIR/workflow.yaml --loglevel debug 60 | -------------------------------------------------------------------------------- /examples/argo/workflow.yaml: -------------------------------------------------------------------------------- 1 | kind: Workflow 2 | apiVersion: v1 3 | metadata: 4 | generateName: git-ghost-argo-example- 5 | labels: 6 | app: git-ghost-argo-example 7 | spec: 8 | arguments: 9 | parameters: 10 | - name: image-prefix 11 | globalName: image-prefix 12 | - name: image-tag 13 | globalName: image-tag 14 | - name: git-repo 15 | globalName: git-repo 16 | - name: git-ghost-repo 17 | globalName: git-ghost-repo 18 | - name: git-ghost-registry 19 | globalName: git-ghost-registry 20 | - name: git-commit-hash 21 | globalName: git-commit-hash 22 | - name: diff-hash 23 | globalName: diff-hash 24 | entrypoint: steps 25 | templates: 26 | - name: steps 27 | steps: 28 | - - name: build-image 29 | template: build-image 30 | - - name: job 31 | template: job 32 | - name: build-image 33 | initContainers: 34 | - name: init 35 | image: "{{workflow.parameters.image-prefix}}git-ghost:{{workflow.parameters.image-tag}}" 36 | imagePullPolicy: IfNotPresent 37 | command: ["bash"] 38 | args: 39 | - "-c" 40 | - "git clone {{workflow.parameters.git-repo}} . && git checkout {{workflow.parameters.git-commit-hash}} && git ghost pull -v {{workflow.parameters.git-commit-hash}} {{workflow.parameters.diff-hash}}" 41 | workingDir: /workspace 42 | env: 43 | - name: GIT_SSH_COMMAND 44 | value: "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i /etc/git-secret/sshkey" 45 | - name: GIT_GHOST_REPO 46 | value: "{{workflow.parameters.git-ghost-repo}}" 47 | mirrorVolumeMounts: true 48 | container: 49 | name: kaniko 50 | image: gcr.io/kaniko-project/executor:v0.9.0 51 | imagePullPolicy: IfNotPresent 52 | args: 53 | - --context=/workspace 54 | - --dockerfile=/workspace/examples/argo/Dockerfile 55 | - --destination={{workflow.parameters.git-ghost-registry}}:{{workflow.parameters.git-commit-hash}}-{{workflow.parameters.diff-hash}} 56 | volumeMounts: 57 | - name: code 58 | mountPath: /workspace 59 | - name: git-secret 60 | mountPath: /etc/git-secret 61 | - name: docker-cred 62 | mountPath: /root 63 | volumes: 64 | - name: code 65 | emptyDir: 66 | - name: git-secret 67 | secret: 68 | secretName: git-ghost-git-cred 69 | defaultMode: 256 70 | - name: docker-cred 71 | projected: 72 | sources: 73 | - secret: 74 | name: git-ghost-docker-cred 75 | items: 76 | - key: .dockerconfigjson 77 | path: .docker/config.json 78 | - name: job 79 | metadata: 80 | labels: 81 | git-commit-hash: "{{workflow.parameters.git-commit-hash}}" 82 | diff-hash: "{{workflow.parameters.diff-hash}}" 83 | container: 84 | image: "{{workflow.parameters.git-ghost-registry}}:{{workflow.parameters.git-commit-hash}}-{{workflow.parameters.diff-hash}}" 85 | command: ["git"] 86 | args: 87 | - diff 88 | - HEAD 89 | imagePullSecrets: 90 | - name: git-ghost-docker-cred 91 | -------------------------------------------------------------------------------- /examples/k8s/pod.yaml: -------------------------------------------------------------------------------- 1 | kind: Pod 2 | apiVersion: v1 3 | metadata: 4 | generateName: git-ghost-example- 5 | labels: 6 | app: git-ghost-example 7 | spec: 8 | initContainers: 9 | - name: init 10 | image: {{IMAGE_PREFIX}}git-ghost:{{IMAGE_TAG}} 11 | imagePullPolicy: IfNotPresent 12 | command: ["bash"] 13 | args: 14 | - "-c" 15 | - "git clone ${GIT_REPO} . && git checkout ${GIT_COMMIT_HASH} && git ghost pull -v ${GIT_COMMIT_HASH} ${DIFF_HASH}" 16 | workingDir: /code 17 | env: 18 | - name: GIT_SSH_COMMAND 19 | value: "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i /etc/git-secret/sshkey" 20 | - name: GIT_REPO 21 | value: {{GIT_REPO}} 22 | - name: GIT_GHOST_REPO 23 | value: {{GIT_GHOST_REPO}} 24 | - name: GIT_COMMIT_HASH 25 | value: {{GIT_COMMIT_HASH}} 26 | - name: DIFF_HASH 27 | value: {{DIFF_HASH}} 28 | volumeMounts: 29 | - name: code 30 | mountPath: /code 31 | - name: git-secret 32 | mountPath: /etc/git-secret 33 | containers: 34 | - name: main 35 | image: {{IMAGE_PREFIX}}git-ghost:{{IMAGE_TAG}} 36 | imagePullPolicy: IfNotPresent 37 | command: ["git", "diff", "HEAD"] 38 | workingDir: /code 39 | volumeMounts: 40 | - name: code 41 | mountPath: /code 42 | volumes: 43 | - name: code 44 | emptyDir: 45 | - name: git-secret 46 | secret: 47 | secretName: git-ghost-creds 48 | defaultMode: 256 49 | restartPolicy: Never 50 | -------------------------------------------------------------------------------- /examples/k8s/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2019 Preferred Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eu 18 | set -o pipefail 19 | 20 | DIR=$(dirname "$0") 21 | 22 | [[ -z "$GIT_GHOST_REPO" ]] && (echo "GIT_GHOST_REPO must be specified."; exit 1) 23 | 24 | kubectl get secret git-ghost-creds > /dev/null || (cat << EOS 25 | git-ghost-creds secret is needed. 26 | You can create it by: 27 | 28 | $ kubectl create secret generic git-ghost-creds --from-file=sshkey=\$HOME/.ssh/id_rsa 29 | 30 | EOS 31 | exit 1) 32 | 33 | IMAGE_PREFIX= 34 | if [[ -z "$IMAGE_PREFIX" ]]; then 35 | IMAGE_PREFIX=ghcr.io/pfnet-research/ 36 | fi 37 | 38 | IMAGE_TAG= 39 | if [[ -z "$IMAGE_TAG" ]]; then 40 | IMAGE_TAG="latest" 41 | fi 42 | 43 | GIT_REPO=`git remote get-url origin` 44 | GIT_COMMIT_HASH=`git rev-parse HEAD` 45 | git diff --exit-code HEAD > /dev/null && (echo "Make some local modification to try the example."; exit 1) 46 | DIFF_HASH=`git ghost push $GIT_COMMIT_HASH | tail -n 1 | cut -f 2 -d " "` 47 | 48 | cat $DIR/pod.yaml | \ 49 | sed "s|{{IMAGE_PREFIX}}|$IMAGE_PREFIX|g" | \ 50 | sed "s|{{IMAGE_TAG}}|$IMAGE_TAG|g" | \ 51 | sed "s|{{GIT_REPO}}|$GIT_REPO|g" | \ 52 | sed "s|{{GIT_GHOST_REPO}}|$GIT_GHOST_REPO|g" | \ 53 | sed "s|{{GIT_COMMIT_HASH}}|$GIT_COMMIT_HASH|g" | \ 54 | sed "s|{{DIFF_HASH}}|$DIFF_HASH|g" | kubectl create -f - 55 | 56 | POD_NAME=`kubectl get pod -l app=git-ghost-example --sort-by=.metadata.creationTimestamp --no-headers -o custom-columns=:metadata.name | tail -n 1` 57 | 58 | sleep 10 59 | 60 | echo "Print modifications synchronized from the local." 61 | echo 62 | 63 | kubectl logs -f $POD_NAME 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pfnet-research/git-ghost 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/hashicorp/go-multierror v1.1.1 7 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 8 | github.com/pkg/errors v0.9.1 9 | github.com/sirupsen/logrus v1.9.0 10 | github.com/spf13/cobra v1.5.0 11 | github.com/spf13/pflag v1.0.5 // indirect 12 | github.com/stretchr/testify v1.8.0 13 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/hashicorp/errwrap v1.0.0 // indirect 19 | github.com/kr/text v0.2.0 // indirect 20 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 7 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 8 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 9 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 10 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 11 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 12 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 15 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 16 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 17 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 18 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 19 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 23 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 24 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 25 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= 26 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 27 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 28 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 29 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 31 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 32 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 33 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 34 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 35 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc= 37 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 40 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 42 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | skip-files: 4 | - "pkg/.*_k8s.go$" 5 | output: 6 | format: colored-line-number 7 | print-issued-lines: true 8 | print-linter-name: true 9 | linters: 10 | enable: 11 | - gofmt 12 | - goimports 13 | - unconvert 14 | - misspell 15 | - interfacer 16 | - maligned 17 | - prealloc 18 | fast: false 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/pfnet-research/git-ghost/cmd" 21 | 22 | log "github.com/sirupsen/logrus" 23 | ) 24 | 25 | func init() { 26 | log.SetOutput(os.Stderr) 27 | } 28 | 29 | func main() { 30 | // RootCmd prints errors if exists 31 | if err := cmd.RootCmd.Execute(); err != nil { 32 | os.Exit(-1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/ghost/delete.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ghost 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | 21 | "github.com/pfnet-research/git-ghost/pkg/ghost/git" 22 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 23 | "github.com/pfnet-research/git-ghost/pkg/util" 24 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 25 | 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | // DeleteOptions represents arg for Delete func 30 | type DeleteOptions struct { 31 | types.WorkingEnvSpec 32 | *types.ListCommitsBranchSpec 33 | *types.ListDiffBranchSpec 34 | Dryrun bool 35 | } 36 | 37 | // DeleteResult contains deleted ghost branches in Delete func 38 | type DeleteResult struct { 39 | *types.CommitsBranches 40 | *types.DiffBranches 41 | } 42 | 43 | // Delete deletes ghost branches from ghost repo and returns deleted branches 44 | func Delete(options DeleteOptions) (*DeleteResult, errors.GitGhostError) { 45 | log.WithFields(util.ToFields(options)).Debug("delete command with") 46 | 47 | res := DeleteResult{} 48 | 49 | if options.ListCommitsBranchSpec != nil { 50 | resolved := options.ListCommitsBranchSpec.Resolve(options.SrcDir) 51 | branches, err := resolved.GetBranches(options.GhostRepo) 52 | if err != nil { 53 | return nil, errors.WithStack(err) 54 | } 55 | res.CommitsBranches = &branches 56 | } 57 | 58 | if options.ListDiffBranchSpec != nil { 59 | resolved := options.ListDiffBranchSpec.Resolve(options.SrcDir) 60 | branches, err := resolved.GetBranches(options.GhostRepo) 61 | if err != nil { 62 | return nil, errors.WithStack(err) 63 | } 64 | res.DiffBranches = &branches 65 | } 66 | 67 | workingEnv, err := options.WorkingEnvSpec.Initialize() 68 | if err != nil { 69 | return nil, errors.WithStack(err) 70 | } 71 | defer util.LogDeferredGitGhostError(workingEnv.Clean) 72 | 73 | deleteBranches := func(branches []types.GhostBranch, dryrun bool) errors.GitGhostError { 74 | if len(branches) == 0 { 75 | return nil 76 | } 77 | var branchNames []string 78 | for _, branch := range branches { 79 | branchNames = append(branchNames, branch.BranchName()) 80 | } 81 | log.WithFields(log.Fields{ 82 | "branches": branchNames, 83 | }).Info("Delete branch") 84 | if dryrun { 85 | return nil 86 | } 87 | err := git.DeleteRemoteBranches(workingEnv.GhostDir, branchNames...) 88 | return errors.WithStack(err) 89 | } 90 | 91 | if res.CommitsBranches != nil { 92 | err := deleteBranches(res.CommitsBranches.AsGhostBranches(), options.Dryrun) 93 | if err != nil { 94 | return nil, errors.WithStack(err) 95 | } 96 | } 97 | 98 | if res.DiffBranches != nil { 99 | err := deleteBranches(res.DiffBranches.AsGhostBranches(), options.Dryrun) 100 | if err != nil { 101 | return nil, errors.WithStack(err) 102 | } 103 | } 104 | 105 | return &res, nil 106 | } 107 | 108 | // PrettyString pretty prints ListResult 109 | func (res *DeleteResult) PrettyString() string { 110 | // TODO: Make it prettier 111 | var buffer bytes.Buffer 112 | if res.CommitsBranches != nil { 113 | buffer.WriteString("Deleted Local Base Branches:\n") 114 | buffer.WriteString("\n") 115 | buffer.WriteString(fmt.Sprintf("%-40s %-40s\n", "Remote Base", "Local Base")) 116 | branches := *res.CommitsBranches 117 | branches.Sort() 118 | for _, branch := range branches { 119 | buffer.WriteString(fmt.Sprintf("%s %s\n", branch.CommitHashFrom, branch.CommitHashTo)) 120 | } 121 | buffer.WriteString("\n") 122 | } 123 | if res.DiffBranches != nil { 124 | buffer.WriteString("Deleted Local Mod Branches:\n") 125 | buffer.WriteString("\n") 126 | buffer.WriteString(fmt.Sprintf("%-40s %-40s\n", "Local Base", "Local Mod")) 127 | branches := *res.DiffBranches 128 | branches.Sort() 129 | for _, branch := range branches { 130 | buffer.WriteString(fmt.Sprintf("%s %s\n", branch.CommitHashFrom, branch.DiffHash)) 131 | } 132 | buffer.WriteString("\n") 133 | } 134 | return buffer.String() 135 | } 136 | -------------------------------------------------------------------------------- /pkg/ghost/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package ghost package is a top package for git-ghost 16 | package ghost // import "github.com/pfnet-research/git-ghost/pkg/ghost" 17 | -------------------------------------------------------------------------------- /pkg/ghost/git/check.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package git 16 | 17 | import ( 18 | "os/exec" 19 | 20 | "github.com/pfnet-research/git-ghost/pkg/util" 21 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 22 | ) 23 | 24 | // ValidateRemoteBranchExistence checks repo has branch or not. 25 | func ValidateRemoteBranchExistence(repo, branch string) (bool, errors.GitGhostError) { 26 | output, err := util.JustOutputCmd( 27 | exec.Command("git", "ls-remote", "--heads", repo, branch), 28 | ) 29 | if err != nil { 30 | return false, err 31 | } 32 | return string(output) != "", nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/ghost/git/conversion.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package git 16 | 17 | import ( 18 | "os/exec" 19 | "strings" 20 | 21 | "github.com/pfnet-research/git-ghost/pkg/util" 22 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 23 | ) 24 | 25 | // ResolveCommittish resolves committish as full commit hash on dir 26 | func ResolveCommittish(dir, committish string) (string, errors.GitGhostError) { 27 | commit, err := util.JustOutputCmd( 28 | exec.Command("git", "-C", dir, "rev-list", "-1", committish), 29 | ) 30 | if err != nil { 31 | return "", err 32 | } 33 | return strings.TrimRight(string(commit), "\r\n"), nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/ghost/git/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package git package contains functions to operate git repositories used by git-ghost 16 | package git // import "github.com/pfnet-research/git-ghost/pkg/ghost/git" 17 | -------------------------------------------------------------------------------- /pkg/ghost/git/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package git 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "os/exec" 21 | 22 | "github.com/pfnet-research/git-ghost/pkg/util" 23 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 24 | 25 | multierror "github.com/hashicorp/go-multierror" 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | // CreateDiffBundleFile creates patches for fromCommittish..toCommittish and save it to filepath 30 | func CreateDiffBundleFile(dir, filepath, fromCommittish, toCommittish string) errors.GitGhostError { 31 | f, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 32 | if err != nil { 33 | return errors.WithStack(err) 34 | } 35 | defer util.LogDeferredError(f.Close) 36 | 37 | cmd := exec.Command("git", "-C", dir, 38 | "log", "-p", "--reverse", "--pretty=email", "--stat", "-m", "--first-parent", "--binary", 39 | fmt.Sprintf("%s..%s", fromCommittish, toCommittish), 40 | ) 41 | cmd.Stdout = f 42 | return util.JustRunCmd(cmd) 43 | } 44 | 45 | // ApplyDiffBundleFile apply a patch file created in CreateDiffBundleFile 46 | func ApplyDiffBundleFile(dir, filepath string) errors.GitGhostError { 47 | var errs error 48 | err := util.JustRunCmd( 49 | exec.Command("git", "-C", dir, "am", filepath), 50 | ) 51 | if err != nil { 52 | errs = multierror.Append(errs, err) 53 | log.WithFields(util.MergeFields( 54 | log.Fields{ 55 | "srcDir": dir, 56 | "filepath": filepath, 57 | "error": err.Error(), 58 | })).Info("apply('git am') failed. aborting.") 59 | resetErr := util.JustRunCmd( 60 | exec.Command("git", "-C", dir, "am", "--abort"), 61 | ) 62 | if resetErr != nil { 63 | errs = multierror.Append(errs, resetErr) 64 | } 65 | } 66 | return errors.WithStack(errs) 67 | } 68 | 69 | // CreateDiffPatchFile creates a diff from committish to current working state of `dir` and save it to filepath 70 | func CreateDiffPatchFile(dir, filepath, committish string) errors.GitGhostError { 71 | f, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 72 | if err != nil { 73 | return errors.WithStack(err) 74 | } 75 | defer util.LogDeferredError(f.Close) 76 | 77 | cmd := exec.Command("git", "-C", dir, "diff", "--patience", "--binary", committish) 78 | cmd.Stdout = f 79 | return util.JustRunCmd(cmd) 80 | } 81 | 82 | // AppendNonIndexedDiffFiles appends non-indexed diff files 83 | func AppendNonIndexedDiffFiles(dir, filepath string, nonIndexedFilepaths []string) errors.GitGhostError { 84 | f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, 0600) 85 | if err != nil { 86 | return errors.WithStack(err) 87 | } 88 | defer util.LogDeferredError(f.Close) 89 | 90 | var errs error 91 | for _, p := range nonIndexedFilepaths { 92 | cmd := exec.Command("git", "-C", dir, "diff", "--patience", "--binary", "--no-index", os.DevNull, p) 93 | cmd.Stdout = f 94 | ggerr := util.JustRunCmd(cmd) 95 | if ggerr != nil { 96 | if util.GetExitCode(ggerr.Cause()) == 1 { 97 | // exit 1 is valid for git diff 98 | continue 99 | } 100 | errs = multierror.Append(errs, ggerr) 101 | } 102 | } 103 | return errors.WithStack(errs) 104 | } 105 | 106 | // ApplyDiffPatchFile apply a diff file created by CreateDiffPatchFile 107 | func ApplyDiffPatchFile(dir, filepath string) errors.GitGhostError { 108 | // Handle empty patch 109 | fi, err := os.Stat(filepath) 110 | if err != nil { 111 | return errors.WithStack(err) 112 | } 113 | if fi.Size() == 0 { 114 | log.WithFields(util.MergeFields( 115 | log.Fields{ 116 | "srcDir": dir, 117 | "filepath": filepath, 118 | })).Info("ignore empty patch") 119 | return nil 120 | } 121 | return util.JustRunCmd( 122 | exec.Command("git", "-C", dir, "apply", filepath), 123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /pkg/ghost/git/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package git 16 | 17 | import ( 18 | "fmt" 19 | "os/exec" 20 | "strings" 21 | 22 | "github.com/pfnet-research/git-ghost/pkg/util" 23 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 24 | ) 25 | 26 | // ListRemoteBranchNames returns remote branch names 27 | func ListRemoteBranchNames(repo string, branchnames []string) ([]string, errors.GitGhostError) { 28 | if len(branchnames) == 0 { 29 | return []string{}, nil 30 | } 31 | 32 | branchNamesToSearch := []string{} 33 | for _, b := range branchnames { 34 | prefixed := b 35 | if !strings.HasPrefix(b, "refs/heads/") { 36 | prefixed = fmt.Sprintf("%s%s", "refs/heads/", b) 37 | } 38 | branchNamesToSearch = append(branchNamesToSearch, prefixed) 39 | } 40 | opts := append([]string{"ls-remote", "-q", "--heads", "--refs", repo}, branchNamesToSearch...) 41 | output, err := util.JustOutputCmd(exec.Command("git", opts...)) 42 | if err != nil { 43 | return []string{}, errors.WithStack(err) 44 | } 45 | 46 | lines := strings.Split(string(output), "\n") 47 | branchNames := make([]string, 0, len(lines)) 48 | for _, line := range lines { 49 | if line == "" { 50 | continue 51 | } 52 | tokens := strings.Fields(line) 53 | if len(tokens) != 2 { 54 | return []string{}, errors.Errorf("Got unexpected line: %s", line) 55 | } 56 | // Assume it starts from "refs/heads/" 57 | name := tokens[1][11:] 58 | branchNames = append(branchNames, name) 59 | } 60 | return branchNames, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/ghost/git/repo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package git 16 | 17 | import ( 18 | "fmt" 19 | "os/exec" 20 | "strings" 21 | 22 | "github.com/pfnet-research/git-ghost/pkg/util" 23 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 24 | 25 | gherrors "github.com/pkg/errors" 26 | ) 27 | 28 | var ( 29 | ORIGIN string = "origin" 30 | ) 31 | 32 | // InitializeGitDir clone repo to dir. 33 | // if you set empty branchname, it will checkout default branch of repo. 34 | func InitializeGitDir(dir, repo, branch string) errors.GitGhostError { 35 | args := []string{"clone", "-q", "-o", ORIGIN} 36 | if branch != "" { 37 | args = append(args, "-b", branch) 38 | } 39 | args = append(args, repo, dir) 40 | cmd := exec.Command("git", args...) 41 | return util.JustRunCmd(cmd) 42 | } 43 | 44 | // CopyUserConfig copies user config from source directory to destination directory. 45 | func CopyUserConfig(srcDir, dstDir string) errors.GitGhostError { 46 | name, email, err := GetUserConfig(srcDir) 47 | if err != nil { 48 | return err 49 | } 50 | return SetUserConfig(dstDir, name, email) 51 | } 52 | 53 | // GetUserConfig returns a user config (name and email) from destination directory. 54 | func GetUserConfig(dir string) (string, string, errors.GitGhostError) { 55 | // Get user config from src 56 | nameBytes, err := util.JustOutputCmd(exec.Command("git", "-C", dir, "config", "user.name")) 57 | if err != nil { 58 | return "", "", errors.WithStack(gherrors.WithMessage(err, "failed to get git user name")) 59 | } 60 | name := strings.TrimSuffix(string(nameBytes), "\n") 61 | emailBytes, err := util.JustOutputCmd(exec.Command("git", "-C", dir, "config", "user.email")) 62 | if err != nil { 63 | return "", "", errors.WithStack(gherrors.WithMessage(err, "failed to get git user email")) 64 | } 65 | email := strings.TrimSuffix(string(emailBytes), "\n") 66 | return name, email, nil 67 | } 68 | 69 | // SetUserConfig sets a user config (name and email) to destination directory. 70 | func SetUserConfig(dir, name, email string) errors.GitGhostError { 71 | // Set the user config to dst 72 | err := util.JustRunCmd(exec.Command("git", "-C", dir, "config", "user.name", fmt.Sprintf("\"%s\"", name))) 73 | if err != nil { 74 | return errors.WithStack(gherrors.WithMessage(err, "failed to set git user name")) 75 | } 76 | err = util.JustRunCmd(exec.Command("git", "-C", dir, "config", "user.email", fmt.Sprintf("\"%s\"", email))) 77 | if err != nil { 78 | return errors.WithStack(gherrors.WithMessage(err, "failed to set git user email")) 79 | } 80 | return nil 81 | } 82 | 83 | // CommitAndPush commits and push to its origin 84 | func CommitAndPush(dir, filename, message, committish string) errors.GitGhostError { 85 | err := CommitFile(dir, filename, message) 86 | if err != nil { 87 | return errors.WithStack(err) 88 | } 89 | err = Push(dir, committish) 90 | if err != nil { 91 | return errors.WithStack(err) 92 | } 93 | return nil 94 | } 95 | 96 | // CommitFile commits a file 97 | func CommitFile(dir, filename, message string) errors.GitGhostError { 98 | err := util.JustRunCmd( 99 | exec.Command("git", "-C", dir, "add", filename), 100 | ) 101 | if err != nil { 102 | return errors.WithStack(err) 103 | } 104 | return util.JustRunCmd( 105 | exec.Command("git", "-C", dir, "commit", "-q", filename, "-m", message), 106 | ) 107 | } 108 | 109 | // DeleteRemoteBranches delete branches from its origin 110 | func DeleteRemoteBranches(dir string, branchNames ...string) errors.GitGhostError { 111 | args := []string{"-C", dir, "push", "origin"} 112 | for _, name := range branchNames { 113 | args = append(args, fmt.Sprintf(":%s", name)) 114 | } 115 | return util.JustRunCmd( 116 | exec.Command("git", args...), 117 | ) 118 | } 119 | 120 | // Push pushes current HEAD to its origin 121 | func Push(dir string, committishes ...string) errors.GitGhostError { 122 | args := []string{"-C", dir, "push", "origin"} 123 | args = append(args, committishes...) 124 | return util.JustRunCmd( 125 | exec.Command("git", args...), 126 | ) 127 | } 128 | 129 | // Pull pulls committish from its origin 130 | func Pull(dir, committish string) errors.GitGhostError { 131 | return util.JustRunCmd( 132 | exec.Command("git", "-C", dir, "pull", "origin", committish), 133 | ) 134 | } 135 | 136 | // CreateOrphanBranch creates an orphan branch on dir 137 | func CreateOrphanBranch(dir, branch string) errors.GitGhostError { 138 | return util.JustRunCmd( 139 | exec.Command("git", "-C", dir, "checkout", "--orphan", branch), 140 | ) 141 | } 142 | 143 | // ResetHardToBranch reset dir to branch with --hard option 144 | func ResetHardToBranch(dir, branch string) errors.GitGhostError { 145 | return util.JustRunCmd( 146 | exec.Command("git", "-C", dir, "reset", "--hard", branch), 147 | ) 148 | } 149 | -------------------------------------------------------------------------------- /pkg/ghost/git/validation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package git 16 | 17 | import ( 18 | "os/exec" 19 | 20 | "github.com/pfnet-research/git-ghost/pkg/util" 21 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 22 | ) 23 | 24 | // ValidateGit check the environment has 'git' command or not. 25 | func ValidateGit() errors.GitGhostError { 26 | return util.JustRunCmd( 27 | exec.Command("git", "version"), 28 | ) 29 | } 30 | 31 | // ValidateCommittish check committish is valid on dir 32 | func ValidateCommittish(dir, committish string) errors.GitGhostError { 33 | output, err := util.JustOutputCmd( 34 | exec.Command("git", "-C", dir, "cat-file", "-e", committish), 35 | ) 36 | if err != nil && util.GetExitCode(err.Cause()) == 1 && len(output) == 0 { 37 | // exit 1 is for unexisting committish. 38 | return errors.Errorf("%s does not exist", committish) 39 | } 40 | return err 41 | } 42 | -------------------------------------------------------------------------------- /pkg/ghost/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ghost 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 23 | "github.com/pfnet-research/git-ghost/pkg/util" 24 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 25 | 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | // ListOptions represents arg for List func 30 | type ListOptions struct { 31 | types.WorkingEnvSpec 32 | *types.ListCommitsBranchSpec 33 | *types.ListDiffBranchSpec 34 | } 35 | 36 | // ListResult contains results of List func 37 | type ListResult struct { 38 | *types.CommitsBranches 39 | *types.DiffBranches 40 | } 41 | 42 | // List returns ghost branches list per ghost branch type 43 | func List(options ListOptions) (*ListResult, errors.GitGhostError) { 44 | log.WithFields(util.ToFields(options)).Debug("list command with") 45 | 46 | res := ListResult{} 47 | 48 | if options.ListCommitsBranchSpec != nil { 49 | resolved := options.ListCommitsBranchSpec.Resolve(options.SrcDir) 50 | branches, err := resolved.GetBranches(options.GhostRepo) 51 | if err != nil { 52 | return nil, errors.WithStack(err) 53 | } 54 | res.CommitsBranches = &branches 55 | } 56 | 57 | if options.ListDiffBranchSpec != nil { 58 | resolved := options.ListDiffBranchSpec.Resolve(options.SrcDir) 59 | branches, err := resolved.GetBranches(options.GhostRepo) 60 | if err != nil { 61 | return nil, errors.WithStack(err) 62 | } 63 | res.DiffBranches = &branches 64 | } 65 | 66 | return &res, nil 67 | } 68 | 69 | // PrettyString pretty prints ListResult 70 | func (res *ListResult) PrettyString(headers bool, output string) string { 71 | // TODO: Make it prettier 72 | var buffer bytes.Buffer 73 | if res.CommitsBranches != nil { 74 | branches := *res.CommitsBranches 75 | branches.Sort() 76 | if headers { 77 | buffer.WriteString("Local Base Branches:\n") 78 | buffer.WriteString("\n") 79 | columns := []string{} 80 | switch output { 81 | case "only-from": 82 | columns = append(columns, fmt.Sprintf("%-40s", "Remote Base")) 83 | case "only-to": 84 | columns = append(columns, fmt.Sprintf("%-40s", "Local Base")) 85 | default: 86 | columns = append(columns, fmt.Sprintf("%-40s", "Remote Base")) 87 | columns = append(columns, fmt.Sprintf("%-40s", "Local Base")) 88 | } 89 | buffer.WriteString(fmt.Sprintf("%s\n", strings.Join(columns, " "))) 90 | } 91 | for _, branch := range branches { 92 | columns := []string{} 93 | switch output { 94 | case "only-from": 95 | columns = append(columns, branch.CommitHashFrom) 96 | case "only-to": 97 | columns = append(columns, branch.CommitHashTo) 98 | default: 99 | columns = append(columns, branch.CommitHashFrom) 100 | columns = append(columns, branch.CommitHashTo) 101 | } 102 | buffer.WriteString(fmt.Sprintf("%s\n", strings.Join(columns, " "))) 103 | } 104 | if headers { 105 | buffer.WriteString("\n") 106 | } 107 | } 108 | if res.DiffBranches != nil { 109 | branches := *res.DiffBranches 110 | branches.Sort() 111 | if headers { 112 | buffer.WriteString("Local Mod Branches:\n") 113 | buffer.WriteString("\n") 114 | columns := []string{} 115 | switch output { 116 | case "only-from": 117 | columns = append(columns, fmt.Sprintf("%-40s", "Local Base")) 118 | case "only-to": 119 | columns = append(columns, fmt.Sprintf("%-40s", "Local Mod")) 120 | default: 121 | columns = append(columns, fmt.Sprintf("%-40s", "Local Base")) 122 | columns = append(columns, fmt.Sprintf("%-40s", "Local Mod")) 123 | } 124 | buffer.WriteString(fmt.Sprintf("%s\n", strings.Join(columns, " "))) 125 | } 126 | for _, branch := range branches { 127 | columns := []string{} 128 | switch output { 129 | case "only-from": 130 | columns = append(columns, branch.CommitHashFrom) 131 | case "only-to": 132 | columns = append(columns, branch.DiffHash) 133 | default: 134 | columns = append(columns, branch.CommitHashFrom) 135 | columns = append(columns, branch.DiffHash) 136 | } 137 | buffer.WriteString(fmt.Sprintf("%s\n", strings.Join(columns, " "))) 138 | } 139 | if headers { 140 | buffer.WriteString("\n") 141 | } 142 | } 143 | return buffer.String() 144 | } 145 | -------------------------------------------------------------------------------- /pkg/ghost/pull.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ghost 16 | 17 | import ( 18 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 19 | "github.com/pfnet-research/git-ghost/pkg/util" 20 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 21 | 22 | log "github.com/sirupsen/logrus" 23 | ) 24 | 25 | // PullOptions represents arg for Pull func 26 | type PullOptions struct { 27 | types.WorkingEnvSpec 28 | *types.CommitsBranchSpec 29 | *types.PullableDiffBranchSpec 30 | } 31 | 32 | func pullAndApply(spec types.PullableGhostBranchSpec, we types.WorkingEnv) errors.GitGhostError { 33 | pulledBranch, err := spec.PullBranch(we) 34 | if err != nil { 35 | return err 36 | } 37 | return pulledBranch.Apply(we) 38 | } 39 | 40 | // Pull pulls ghost branches and apply to workind directory 41 | func Pull(options PullOptions) errors.GitGhostError { 42 | log.WithFields(util.ToFields(options)).Debug("pull command with") 43 | we, err := options.WorkingEnvSpec.Initialize() 44 | if err != nil { 45 | return errors.WithStack(err) 46 | } 47 | defer util.LogDeferredGitGhostError(we.Clean) 48 | 49 | if options.CommitsBranchSpec != nil { 50 | err := pullAndApply(*options.CommitsBranchSpec, *we) 51 | if err != nil { 52 | return errors.WithStack(err) 53 | } 54 | } 55 | 56 | if options.PullableDiffBranchSpec != nil { 57 | err := pullAndApply(*options.PullableDiffBranchSpec, *we) 58 | return errors.WithStack(err) 59 | } 60 | 61 | log.WithFields(util.ToFields(options)).Warn("pull command has nothing to do with") 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/ghost/push.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ghost 16 | 17 | import ( 18 | "github.com/pfnet-research/git-ghost/pkg/ghost/git" 19 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 20 | "github.com/pfnet-research/git-ghost/pkg/util" 21 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 22 | 23 | log "github.com/sirupsen/logrus" 24 | ) 25 | 26 | // PushOptions represents arg for Push func 27 | type PushOptions struct { 28 | types.WorkingEnvSpec 29 | *types.CommitsBranchSpec 30 | *types.DiffBranchSpec 31 | } 32 | 33 | // PushResult contains resultant ghost branches of Push func 34 | type PushResult struct { 35 | *types.CommitsBranch 36 | *types.DiffBranch 37 | } 38 | 39 | // Push pushes create ghost branches and push them to remote ghost repository 40 | func Push(options PushOptions) (*PushResult, errors.GitGhostError) { 41 | log.WithFields(util.ToFields(options)).Debug("push command with") 42 | 43 | var result PushResult 44 | if options.CommitsBranchSpec != nil { 45 | branch, err := pushGhostBranch(options.CommitsBranchSpec, options.WorkingEnvSpec) 46 | if err != nil { 47 | return nil, errors.WithStack(err) 48 | } 49 | commitsBranch, _ := branch.(*types.CommitsBranch) 50 | result.CommitsBranch = commitsBranch 51 | } 52 | 53 | if options.DiffBranchSpec != nil { 54 | branch, err := pushGhostBranch(options.DiffBranchSpec, options.WorkingEnvSpec) 55 | if err != nil { 56 | return nil, errors.WithStack(err) 57 | } 58 | diffBranch, _ := branch.(*types.DiffBranch) 59 | result.DiffBranch = diffBranch 60 | } 61 | 62 | return &result, nil 63 | } 64 | 65 | func pushGhostBranch(branchSpec types.GhostBranchSpec, workingEnvSpec types.WorkingEnvSpec) (types.GhostBranch, errors.GitGhostError) { 66 | workingEnv, err := workingEnvSpec.Initialize() 67 | if err != nil { 68 | return nil, errors.WithStack(err) 69 | } 70 | defer util.LogDeferredGitGhostError(workingEnv.Clean) 71 | dstDir := workingEnv.GhostDir 72 | branch, err := branchSpec.CreateBranch(*workingEnv) 73 | if err != nil { 74 | return nil, errors.WithStack(err) 75 | } 76 | if branch == nil { 77 | return nil, nil 78 | } 79 | existence, err := git.ValidateRemoteBranchExistence( 80 | workingEnv.GhostRepo, 81 | branch.BranchName(), 82 | ) 83 | if err != nil { 84 | return nil, errors.WithStack(err) 85 | } 86 | if existence { 87 | log.WithFields(log.Fields{ 88 | "branch": branch.BranchName(), 89 | "ghostRepo": workingEnv.GhostRepo, 90 | }).Info("skipped pushing existing branch") 91 | return branch, nil 92 | } 93 | 94 | log.WithFields(log.Fields{ 95 | "branch": branch.BranchName(), 96 | "ghostRepo": workingEnv.GhostRepo, 97 | }).Info("pushing branch") 98 | err = git.Push(dstDir, branch.BranchName()) 99 | if err != nil { 100 | return nil, errors.WithStack(err) 101 | } 102 | return branch, nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/ghost/show.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ghost 16 | 17 | import ( 18 | "io" 19 | 20 | "github.com/pfnet-research/git-ghost/pkg/ghost/types" 21 | "github.com/pfnet-research/git-ghost/pkg/util" 22 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 23 | 24 | log "github.com/sirupsen/logrus" 25 | ) 26 | 27 | // ShowOptions represents arg for Pull func 28 | type ShowOptions struct { 29 | types.WorkingEnvSpec 30 | *types.CommitsBranchSpec 31 | *types.PullableDiffBranchSpec 32 | // if you want to consume and transform the output of `ghost.Show()`, 33 | // Please use `io.Pipe()` as below, 34 | // ``` 35 | // r, w := io.Pipe() 36 | // go func() { ghost.Show(ShowOptions{ Writer: w }); w.Close()} 37 | // ```` 38 | // Then, you can read the output from `r` and transform them as you like. 39 | Writer io.Writer 40 | } 41 | 42 | func pullAndshow(branchSpec types.PullableGhostBranchSpec, we types.WorkingEnv, writer io.Writer) errors.GitGhostError { 43 | branch, err := branchSpec.PullBranch(we) 44 | if err != nil { 45 | return err 46 | } 47 | return branch.Show(we, writer) 48 | } 49 | 50 | // Show writes ghost branches contents to option.Writer 51 | func Show(options ShowOptions) errors.GitGhostError { 52 | log.WithFields(util.ToFields(options)).Debug("pull command with") 53 | 54 | if options.CommitsBranchSpec != nil { 55 | we, err := options.WorkingEnvSpec.Initialize() 56 | if err != nil { 57 | return err 58 | } 59 | defer util.LogDeferredGitGhostError(we.Clean) 60 | err = pullAndshow(options.CommitsBranchSpec, *we, options.Writer) 61 | if err != nil { 62 | return err 63 | } 64 | } 65 | 66 | if options.PullableDiffBranchSpec != nil { 67 | we, err := options.WorkingEnvSpec.Initialize() 68 | if err != nil { 69 | return err 70 | } 71 | defer util.LogDeferredGitGhostError(we.Clean) 72 | return pullAndshow(options.PullableDiffBranchSpec, *we, options.Writer) 73 | } 74 | 75 | log.WithFields(util.ToFields(options)).Warn("show command has nothing to do with") 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/ghost/types/branch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package types 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "os/exec" 21 | "path" 22 | "reflect" 23 | "regexp" 24 | "sort" 25 | 26 | "github.com/pfnet-research/git-ghost/pkg/ghost/git" 27 | "github.com/pfnet-research/git-ghost/pkg/util" 28 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 29 | 30 | log "github.com/sirupsen/logrus" 31 | ) 32 | 33 | // GhostBranch is an interface representing a ghost branch. 34 | // 35 | // It is created from GhostBranchSpec/PullableGhostBranchSpec 36 | type GhostBranch interface { 37 | // BranchName returns its full branch name on git repository 38 | BranchName() string 39 | // FileName returns a file name contained in the GhostBranch 40 | FileName() string 41 | // Show writes contents of this ghost branch on passed working env to writer 42 | Show(we WorkingEnv, writer io.Writer) errors.GitGhostError 43 | // Apply applies contents(diff or patch) of this ghost branch on passed working env 44 | Apply(we WorkingEnv) errors.GitGhostError 45 | } 46 | 47 | // interface assetions 48 | var _ GhostBranch = CommitsBranch{} 49 | var _ GhostBranch = DiffBranch{} 50 | 51 | // CommitsBranch represents a local base branch 52 | // 53 | // This contains patches for CommitHashFrom..CommitHashTo 54 | type CommitsBranch struct { 55 | Prefix string 56 | CommitHashFrom string 57 | CommitHashTo string 58 | } 59 | 60 | // DiffBranch represents a local mod branch 61 | // 62 | // This contains diff 63 | // - whose content hash value is DiffHash 64 | // - which is generated on CommitHashFrom 65 | type DiffBranch struct { 66 | // Prefix is a prefix of branch name 67 | Prefix string 68 | // CommitHashFrom is full commit hash to which this local mod branch's diff contains 69 | CommitHashFrom string 70 | // DiffHash is a hash value of its diff 71 | DiffHash string 72 | } 73 | 74 | // CommitsBranches is an alias for []CommitsBranch 75 | type CommitsBranches []CommitsBranch 76 | 77 | // DiffBranches is an alias for []DiffBranch 78 | type DiffBranches []DiffBranch 79 | 80 | var commitsBranchNamePattern = regexp.MustCompile(`^([a-z0-9]+)/([a-f0-9]+)-([a-f0-9]+)$`) 81 | var diffBranchNamePattern = regexp.MustCompile(`^([a-z0-9]+)/([a-f0-9]+)/([a-f0-9]+)$`) 82 | 83 | // BranchName returns its full branch name on git repository 84 | func (b CommitsBranch) BranchName() string { 85 | return fmt.Sprintf("%s/%s-%s", b.Prefix, b.CommitHashFrom, b.CommitHashTo) 86 | } 87 | 88 | // FileName returns a file name containing this GhostBranch 89 | func (b CommitsBranch) FileName() string { 90 | return "commits.patch" 91 | } 92 | 93 | // BranchName returns its full branch name on git repository 94 | func (b DiffBranch) BranchName() string { 95 | return fmt.Sprintf("%s/%s/%s", b.Prefix, b.CommitHashFrom, b.DiffHash) 96 | } 97 | 98 | // FileName returns a file name containing this GhostBranch 99 | func (b DiffBranch) FileName() string { 100 | return "local-mod.patch" 101 | } 102 | 103 | // CreateGhostBranchByName instantiates GhostBranch object from branchname 104 | func CreateGhostBranchByName(branchName string) GhostBranch { 105 | m := commitsBranchNamePattern.FindStringSubmatch(branchName) 106 | if len(m) > 0 { 107 | return &CommitsBranch{ 108 | Prefix: m[1], 109 | CommitHashFrom: m[2], 110 | CommitHashTo: m[3], 111 | } 112 | } 113 | m = diffBranchNamePattern.FindStringSubmatch(branchName) 114 | if len(m) > 0 { 115 | return &DiffBranch{ 116 | Prefix: m[1], 117 | CommitHashFrom: m[2], 118 | DiffHash: m[3], 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | // Sort sorts passed branches in lexicographic order of BranchName() 125 | func (branches CommitsBranches) Sort() { 126 | sortFunc := func(i, j int) bool { 127 | return branches[i].BranchName() < branches[j].BranchName() 128 | } 129 | sort.Slice(branches, sortFunc) 130 | } 131 | 132 | // AsGhostBranches just lifts item type to GhostBranch 133 | func (branches CommitsBranches) AsGhostBranches() []GhostBranch { 134 | ghostBranches := make([]GhostBranch, len(branches)) 135 | for i, branch := range branches { 136 | ghostBranches[i] = branch 137 | } 138 | return ghostBranches 139 | } 140 | 141 | // Sort sorts passed branches in lexicographic order of BranchName() 142 | func (branches DiffBranches) Sort() { 143 | sortFunc := func(i, j int) bool { 144 | return branches[i].BranchName() < branches[j].BranchName() 145 | } 146 | sort.Slice(branches, sortFunc) 147 | } 148 | 149 | // AsGhostBranches just lifts item type to GhostBranch 150 | func (branches DiffBranches) AsGhostBranches() []GhostBranch { 151 | ghostBranches := make([]GhostBranch, len(branches)) 152 | for i, branch := range branches { 153 | ghostBranches[i] = branch 154 | } 155 | return ghostBranches 156 | } 157 | 158 | func show(ghost GhostBranch, we WorkingEnv, writer io.Writer) errors.GitGhostError { 159 | cmd := exec.Command("git", "-C", we.GhostDir, "--no-pager", "cat-file", "-p", fmt.Sprintf("HEAD:%s", ghost.FileName())) 160 | cmd.Stdout = writer 161 | return util.JustRunCmd(cmd) 162 | } 163 | 164 | func apply(ghost GhostBranch, we WorkingEnv, expectedSrcHead string) errors.GitGhostError { 165 | log.WithFields(util.MergeFields( 166 | util.ToFields(ghost), 167 | log.Fields{ 168 | "ghostDir": we.GhostDir, 169 | "srcDir": we.SrcDir, 170 | "expectedSrcHead": expectedSrcHead, 171 | }, 172 | )).Info("applying ghost branch") 173 | 174 | srcHead, err := git.ResolveCommittish(we.SrcDir, "HEAD") 175 | if err != nil { 176 | return err 177 | } 178 | 179 | if srcHead != expectedSrcHead { 180 | message := "HEAD is not equal to expected" 181 | log.WithFields(util.MergeFields( 182 | util.ToFields(ghost), 183 | log.Fields{ 184 | "actualSrcHead": srcHead, 185 | "expectedSrcHead": expectedSrcHead, 186 | "srcDir": we.SrcDir, 187 | }, 188 | ), 189 | ).Warnf("%s. Applying ghost branch might be failed.", message) 190 | } 191 | 192 | // TODO make this instance methods. 193 | switch ghost.(type) { 194 | case CommitsBranch: 195 | return git.ApplyDiffBundleFile(we.SrcDir, path.Join(we.GhostDir, ghost.FileName())) 196 | case DiffBranch: 197 | return git.ApplyDiffPatchFile(we.SrcDir, path.Join(we.GhostDir, ghost.FileName())) 198 | default: 199 | return errors.Errorf("not supported on type = %+v", reflect.TypeOf(ghost)) 200 | } 201 | } 202 | 203 | // Show writes contents of this ghost branch on passed working env to writer 204 | func (bs CommitsBranch) Show(we WorkingEnv, writer io.Writer) errors.GitGhostError { 205 | return show(bs, we, writer) 206 | } 207 | 208 | // Apply applies contents(diff or patch) of this ghost branch on passed working env 209 | func (bs CommitsBranch) Apply(we WorkingEnv) errors.GitGhostError { 210 | if bs.CommitHashFrom == bs.CommitHashTo { 211 | log.WithFields(log.Fields{ 212 | "from": bs.CommitHashFrom, 213 | "to": bs.CommitHashTo, 214 | }).Warn("skipping apply ghost commits branch because from-hash and to-hash is the same.") 215 | return nil 216 | } 217 | err := apply(bs, we, bs.CommitHashFrom) 218 | if err != nil { 219 | return err 220 | } 221 | return nil 222 | } 223 | 224 | // Show writes contents of this ghost branch on passed working env to writer 225 | func (bs DiffBranch) Show(we WorkingEnv, writer io.Writer) errors.GitGhostError { 226 | return show(bs, we, writer) 227 | } 228 | 229 | // Apply applies contents(diff or patch) of this ghost branch on passed working env 230 | func (bs DiffBranch) Apply(we WorkingEnv) errors.GitGhostError { 231 | err := apply(bs, we, bs.CommitHashFrom) 232 | if err != nil { 233 | return err 234 | } 235 | return nil 236 | } 237 | -------------------------------------------------------------------------------- /pkg/ghost/types/branchspec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package types 16 | 17 | import ( 18 | "os" 19 | "path/filepath" 20 | "strings" 21 | 22 | "github.com/pfnet-research/git-ghost/pkg/ghost/git" 23 | "github.com/pfnet-research/git-ghost/pkg/util" 24 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 25 | "github.com/pfnet-research/git-ghost/pkg/util/hash" 26 | 27 | multierror "github.com/hashicorp/go-multierror" 28 | log "github.com/sirupsen/logrus" 29 | ) 30 | 31 | // GhostBranchSpec is an interface 32 | // 33 | // GhostBranchSpec is a specification for creating ghost branch 34 | type GhostBranchSpec interface { 35 | // CreateBranch create a ghost branch on WorkingEnv and returns a GhostBranch object 36 | CreateBranch(we WorkingEnv) (GhostBranch, errors.GitGhostError) 37 | } 38 | 39 | // PullableGhostBranchSpec is an interface 40 | // 41 | // PullableGhostBranchSpec is a specification for pulling ghost branch from ghost repo 42 | type PullableGhostBranchSpec interface { 43 | // PullBranch pulls a ghost branch on from ghost repo in WorkingEnv and returns a GhostBranch object 44 | PullBranch(we WorkingEnv) (GhostBranch, errors.GitGhostError) 45 | } 46 | 47 | // ensuring interfaces 48 | var _ GhostBranchSpec = CommitsBranchSpec{} 49 | var _ GhostBranchSpec = DiffBranchSpec{} 50 | var _ PullableGhostBranchSpec = CommitsBranchSpec{} 51 | var _ PullableGhostBranchSpec = PullableDiffBranchSpec{} 52 | 53 | // Constants 54 | const maxSymlinkDepth = 3 55 | 56 | // CommitsBranchSpec is a spec for creating local base branch 57 | type CommitsBranchSpec struct { 58 | Prefix string 59 | CommittishFrom string 60 | CommittishTo string 61 | } 62 | 63 | // DiffBranchSpec is a spec for creating local mod branch 64 | type DiffBranchSpec struct { 65 | Prefix string 66 | CommittishFrom string 67 | IncludedFilepaths []string 68 | FollowSymlinks bool 69 | } 70 | 71 | // PullableDiffBranchSpec is a spec for pulling local base branch 72 | type PullableDiffBranchSpec struct { 73 | Prefix string 74 | CommittishFrom string 75 | DiffHash string 76 | } 77 | 78 | // Resolve resolves committish in DiffBranchSpec as full commit hash values 79 | func (bs CommitsBranchSpec) Resolve(srcDir string) (*CommitsBranchSpec, errors.GitGhostError) { 80 | err := git.ValidateCommittish(srcDir, bs.CommittishFrom) 81 | if err != nil { 82 | return nil, err 83 | } 84 | commitHashFrom := resolveCommittishOr(srcDir, bs.CommittishFrom) 85 | err = git.ValidateCommittish(srcDir, bs.CommittishTo) 86 | if err != nil { 87 | return nil, err 88 | } 89 | commitHashTo := resolveCommittishOr(srcDir, bs.CommittishTo) 90 | branch := &CommitsBranchSpec{ 91 | Prefix: bs.Prefix, 92 | CommittishFrom: commitHashFrom, 93 | CommittishTo: commitHashTo, 94 | } 95 | return branch, nil 96 | } 97 | 98 | // PullBranch pulls a ghost branch on from ghost repo in WorkingEnv and returns a GhostBranch object 99 | func (bs CommitsBranchSpec) PullBranch(we WorkingEnv) (GhostBranch, errors.GitGhostError) { 100 | resolved, err := bs.Resolve(we.SrcDir) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | branch := &CommitsBranch{ 106 | Prefix: resolved.Prefix, 107 | CommitHashFrom: resolved.CommittishFrom, 108 | CommitHashTo: resolved.CommittishTo, 109 | } 110 | err = pull(branch, we) 111 | if err != nil { 112 | return nil, err 113 | } 114 | return branch, nil 115 | } 116 | 117 | // CreateBranch create a ghost branch on WorkingEnv and returns a GhostBranch object 118 | func (bs CommitsBranchSpec) CreateBranch(we WorkingEnv) (GhostBranch, errors.GitGhostError) { 119 | dstDir := we.GhostDir 120 | srcDir := we.SrcDir 121 | resolved, ggerr := bs.Resolve(we.SrcDir) 122 | if ggerr != nil { 123 | return nil, ggerr 124 | } 125 | 126 | commitHashFrom := resolved.CommittishFrom 127 | commitHashTo := resolved.CommittishTo 128 | branch := CommitsBranch{ 129 | Prefix: resolved.Prefix, 130 | CommitHashFrom: commitHashFrom, 131 | CommitHashTo: commitHashTo, 132 | } 133 | tmpFile, err := os.CreateTemp("", "git-ghost-local-base") 134 | if err != nil { 135 | return nil, errors.WithStack(err) 136 | } 137 | util.LogDeferredError(tmpFile.Close) 138 | defer util.LogDeferredError(func() error { return os.Remove(tmpFile.Name()) }) 139 | ggerr = git.CreateDiffBundleFile(srcDir, tmpFile.Name(), commitHashFrom, commitHashTo) 140 | if ggerr != nil { 141 | return nil, ggerr 142 | } 143 | err = os.Rename(tmpFile.Name(), filepath.Join(dstDir, branch.FileName())) 144 | if err != nil { 145 | return nil, errors.WithStack(err) 146 | } 147 | 148 | ggerr = git.CreateOrphanBranch(dstDir, branch.BranchName()) 149 | if ggerr != nil { 150 | return nil, ggerr 151 | } 152 | ggerr = git.CommitFile(dstDir, branch.FileName(), "Create ghost commit") 153 | if ggerr != nil { 154 | return nil, ggerr 155 | } 156 | 157 | return &branch, nil 158 | } 159 | 160 | // Resolve resolves committish in DiffBranchSpec as full commit hash values 161 | func (bs DiffBranchSpec) Resolve(srcDir string) (*DiffBranchSpec, errors.GitGhostError) { 162 | err := git.ValidateCommittish(srcDir, bs.CommittishFrom) 163 | if err != nil { 164 | return nil, err 165 | } 166 | commitHashFrom := resolveCommittishOr(srcDir, bs.CommittishFrom) 167 | 168 | var errs error 169 | includedFilepaths := make([]string, 0, len(bs.IncludedFilepaths)) 170 | for _, p := range bs.IncludedFilepaths { 171 | resolved, err := resolveFilepath(srcDir, p) 172 | if err != nil { 173 | errs = multierror.Append(errs, err) 174 | continue 175 | } 176 | includedFilepaths = append(includedFilepaths, resolved) 177 | 178 | if bs.FollowSymlinks { 179 | islink, err := util.IsSymlink(p) 180 | if err != nil { 181 | errs = multierror.Append(errs, err) 182 | continue 183 | } 184 | if islink { 185 | err := util.WalkSymlink(srcDir, p, func(paths []string, pp string) errors.GitGhostError { 186 | if len(paths) > maxSymlinkDepth { 187 | return errors.Errorf("symlink is too deep (< %d): %s", maxSymlinkDepth, strings.Join(paths, " -> ")) 188 | } 189 | if filepath.IsAbs(pp) { 190 | return errors.Errorf("symlink to absolute path is not supported: %s -> %s", strings.Join(paths, " -> "), pp) 191 | } 192 | resolved, err := resolveFilepath(srcDir, pp) 193 | if err != nil { 194 | return errors.WithStack(err) 195 | } 196 | includedFilepaths = append(includedFilepaths, resolved) 197 | return nil 198 | }) 199 | if err != nil { 200 | errs = multierror.Append(errs, err) 201 | continue 202 | } 203 | } 204 | } 205 | } 206 | if errs != nil { 207 | return nil, errors.WithStack(errs) 208 | } 209 | if len(includedFilepaths) > 0 { 210 | includedFilepaths = util.UniqueStringSlice(includedFilepaths) 211 | } 212 | 213 | return &DiffBranchSpec{ 214 | Prefix: bs.Prefix, 215 | CommittishFrom: commitHashFrom, 216 | IncludedFilepaths: includedFilepaths, 217 | }, nil 218 | } 219 | 220 | // CreateBranch create a ghost branch on WorkingEnv and returns a GhostBranch object 221 | func (bs DiffBranchSpec) CreateBranch(we WorkingEnv) (GhostBranch, errors.GitGhostError) { 222 | dstDir := we.GhostDir 223 | srcDir := we.SrcDir 224 | resolved, ggerr := bs.Resolve(we.SrcDir) 225 | if ggerr != nil { 226 | return nil, ggerr 227 | } 228 | commitHashFrom := resolved.CommittishFrom 229 | tmpFile, err := os.CreateTemp("", "git-ghost-local-mod") 230 | if err != nil { 231 | return nil, errors.WithStack(err) 232 | } 233 | util.LogDeferredError(tmpFile.Close) 234 | defer util.LogDeferredError(func() error { return os.Remove(tmpFile.Name()) }) 235 | err = git.CreateDiffPatchFile(srcDir, tmpFile.Name(), commitHashFrom) 236 | if err != nil { 237 | return nil, errors.WithStack(err) 238 | } 239 | 240 | if len(bs.IncludedFilepaths) > 0 { 241 | err = git.AppendNonIndexedDiffFiles(srcDir, tmpFile.Name(), resolved.IncludedFilepaths) 242 | if err != nil { 243 | return nil, errors.WithStack(err) 244 | } 245 | } 246 | 247 | hash, err := hash.GenerateFileContentHash(tmpFile.Name()) 248 | if err != nil { 249 | return nil, errors.WithStack(err) 250 | } 251 | branch := DiffBranch{ 252 | Prefix: resolved.Prefix, 253 | CommitHashFrom: commitHashFrom, 254 | DiffHash: hash, 255 | } 256 | err = os.Rename(tmpFile.Name(), filepath.Join(dstDir, branch.FileName())) 257 | if err != nil { 258 | return nil, errors.WithStack(err) 259 | } 260 | 261 | err = git.CreateOrphanBranch(dstDir, branch.BranchName()) 262 | if err != nil { 263 | return nil, errors.WithStack(err) 264 | } 265 | err = git.CommitFile(dstDir, branch.FileName(), "Create ghost commit") 266 | if err != nil { 267 | return nil, errors.WithStack(err) 268 | } 269 | 270 | return &branch, nil 271 | } 272 | 273 | // Resolve resolves committish in PullableDiffBranchSpec as full commit hash values 274 | func (bs PullableDiffBranchSpec) Resolve(srcDir string) (*PullableDiffBranchSpec, errors.GitGhostError) { 275 | err := git.ValidateCommittish(srcDir, bs.CommittishFrom) 276 | if err != nil { 277 | return nil, errors.WithStack(err) 278 | } 279 | commitHashFrom := resolveCommittishOr(srcDir, bs.CommittishFrom) 280 | 281 | return &PullableDiffBranchSpec{ 282 | Prefix: bs.Prefix, 283 | CommittishFrom: commitHashFrom, 284 | DiffHash: bs.DiffHash, 285 | }, nil 286 | } 287 | 288 | // PullBranch pulls a ghost branch on from ghost repo in WorkingEnv and returns a GhostBranch object 289 | func (bs PullableDiffBranchSpec) PullBranch(we WorkingEnv) (GhostBranch, errors.GitGhostError) { 290 | resolved, err := bs.Resolve(we.SrcDir) 291 | if err != nil { 292 | return nil, err 293 | } 294 | branch := &DiffBranch{ 295 | Prefix: resolved.Prefix, 296 | CommitHashFrom: resolved.CommittishFrom, 297 | DiffHash: bs.DiffHash, 298 | } 299 | err = pull(branch, we) 300 | if err != nil { 301 | return nil, err 302 | } 303 | return branch, nil 304 | } 305 | 306 | func pull(ghost GhostBranch, we WorkingEnv) errors.GitGhostError { 307 | return git.ResetHardToBranch(we.GhostDir, git.ORIGIN+"/"+ghost.BranchName()) 308 | } 309 | 310 | func resolveCommittishOr(srcDir string, committishToResolve string) string { 311 | resolved, err := git.ResolveCommittish(srcDir, committishToResolve) 312 | if err != nil { 313 | log.WithFields(log.Fields{ 314 | "repository": srcDir, 315 | "specified": committishToResolve, 316 | }).Warn("can't resolve commit-ish value on local git repository. specified commit-ish value will be used.") 317 | return committishToResolve 318 | } 319 | return resolved 320 | } 321 | 322 | func resolveFilepath(dir, p string) (string, errors.GitGhostError) { 323 | absp := p 324 | if !filepath.IsAbs(p) { 325 | absp = filepath.Clean(filepath.Join(dir, p)) 326 | } 327 | relp, err := filepath.Rel(dir, absp) 328 | if err != nil { 329 | return "", errors.WithStack(err) 330 | } 331 | log.WithFields(log.Fields{ 332 | "dir": dir, 333 | "path": p, 334 | "absp": absp, 335 | "relp": relp, 336 | }).Debugf("resolved path") 337 | if strings.HasPrefix(relp, "../") { 338 | return "", errors.Errorf("%s is not located in the source directory", p) 339 | } 340 | isdir, err := util.IsDir(relp) 341 | if err != nil { 342 | return "", errors.WithStack(err) 343 | } 344 | if isdir { 345 | return "", errors.Errorf("directory diff is not supported: %s", p) 346 | } 347 | return relp, nil 348 | } 349 | -------------------------------------------------------------------------------- /pkg/ghost/types/listbranchspec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package types 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/pfnet-research/git-ghost/pkg/ghost/git" 21 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 22 | ) 23 | 24 | // ListCommitsBranchSpec is spec for list commits branch 25 | type ListCommitsBranchSpec struct { 26 | // Prefix is a prefix of branch name 27 | Prefix string 28 | // HashFrom is committish value to list HashFrom..HashTo 29 | HashFrom string 30 | // HashTo is a committish value to list HashFrom..HashTo 31 | HashTo string 32 | } 33 | 34 | // ListCommitsBranchSpec is spec for list diff branch 35 | type ListDiffBranchSpec struct { 36 | Prefix string 37 | // HashFrom is committish value to list HashFrom..HashTo 38 | HashFrom string 39 | // HashTo is a committish value to list HashFrom..HashTo 40 | HashTo string 41 | } 42 | 43 | // Resolve resolves committish values in ListCommitsBranchSpec as full commit hash 44 | func (ls *ListCommitsBranchSpec) Resolve(srcDir string) *ListCommitsBranchSpec { 45 | newSpec := *ls 46 | if ls.HashFrom != "" { 47 | newSpec.HashFrom = resolveCommittishOr(srcDir, ls.HashFrom) 48 | } 49 | if ls.HashTo != "" { 50 | newSpec.HashTo = resolveCommittishOr(srcDir, ls.HashTo) 51 | } 52 | return &newSpec 53 | } 54 | 55 | // GetBranches returns CommitsBranches from spec 56 | func (ls *ListCommitsBranchSpec) GetBranches(repo string) (CommitsBranches, errors.GitGhostError) { 57 | branchNames, err := listGhostBranchNames(repo, ls.Prefix, ls.HashFrom, ls.HashTo) 58 | if err != nil { 59 | return nil, errors.WithStack(err) 60 | } 61 | var branches CommitsBranches 62 | for _, name := range branchNames { 63 | branch := CreateGhostBranchByName(name) 64 | if br, ok := branch.(*CommitsBranch); ok { 65 | branches = append(branches, *br) 66 | } 67 | } 68 | return branches, nil 69 | } 70 | 71 | // Resolve resolves committish values in ListDiffBranchSpec as full commit hash 72 | func (ls *ListDiffBranchSpec) Resolve(srcDir string) *ListDiffBranchSpec { 73 | newSpec := *ls 74 | if ls.HashFrom != "" { 75 | newSpec.HashFrom = resolveCommittishOr(srcDir, ls.HashFrom) 76 | } 77 | return &newSpec 78 | } 79 | 80 | // GetBranches returns DiffBranches from spec 81 | func (ls *ListDiffBranchSpec) GetBranches(repo string) (DiffBranches, errors.GitGhostError) { 82 | branchNames, err := listGhostBranchNames(repo, ls.Prefix, ls.HashFrom, ls.HashTo) 83 | if err != nil { 84 | return nil, errors.WithStack(err) 85 | } 86 | var branches DiffBranches 87 | for _, name := range branchNames { 88 | branch := CreateGhostBranchByName(name) 89 | if br, ok := branch.(*DiffBranch); ok { 90 | branches = append(branches, *br) 91 | } 92 | } 93 | return branches, nil 94 | } 95 | 96 | func listGhostBranchNames(repo, prefix, fromCommittish, toCommittish string) ([]string, error) { 97 | fromPattern := "*" 98 | toPattern := "*" 99 | if fromCommittish != "" { 100 | fromPattern = fromCommittish 101 | } 102 | if toCommittish != "" { 103 | toPattern = toCommittish 104 | } 105 | 106 | branchNames, err := git.ListRemoteBranchNames(repo, []string{ 107 | fmt.Sprintf("%s/%s-%s", prefix, fromPattern, toPattern), 108 | fmt.Sprintf("%s/%s/%s", prefix, fromPattern, toPattern), 109 | }) 110 | if err != nil { 111 | return []string{}, errors.WithStack(err) 112 | } 113 | 114 | return branchNames, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/ghost/types/workingenv.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package types 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/pfnet-research/git-ghost/pkg/ghost/git" 21 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 22 | 23 | log "github.com/sirupsen/logrus" 24 | ) 25 | 26 | const ( 27 | defaultGhostUserName = "Git Ghost" 28 | defaultGhostUserEmail = "git-ghost@example.com" 29 | ) 30 | 31 | // WorkingEnvSpec abstract an environment git-ghost works with 32 | type WorkingEnvSpec struct { 33 | // SrcDir is local git directory 34 | SrcDir string 35 | // GhostWorkingDir is a root directory which git-ghost creates temporary directories 36 | GhostWorkingDir string 37 | // GhostRepo is a repository url git-ghost works with 38 | GhostRepo string 39 | // GhostUserName is a user name which is used in ghost working directories. 40 | GhostUserName string 41 | // GhostUserEmail is a user email which is used in ghost working directories. 42 | GhostUserEmail string 43 | } 44 | 45 | // WorkingEnv is initialized environment containing temporary local ghost repository 46 | type WorkingEnv struct { 47 | WorkingEnvSpec 48 | GhostDir string 49 | } 50 | 51 | func (weSpec WorkingEnvSpec) Initialize() (*WorkingEnv, errors.GitGhostError) { 52 | ghostDir, err := os.MkdirTemp(weSpec.GhostWorkingDir, "git-ghost-") 53 | if err != nil { 54 | return nil, errors.WithStack(err) 55 | } 56 | ggerr := git.InitializeGitDir(ghostDir, weSpec.GhostRepo, "") 57 | if ggerr != nil { 58 | return nil, ggerr 59 | } 60 | ghostUserName := defaultGhostUserName 61 | if weSpec.GhostUserName != "" { 62 | ghostUserName = weSpec.GhostUserName 63 | } 64 | ghostUserEmail := defaultGhostUserEmail 65 | if weSpec.GhostUserEmail != "" { 66 | ghostUserEmail = weSpec.GhostUserEmail 67 | } 68 | ggerr = git.SetUserConfig(ghostDir, ghostUserName, ghostUserEmail) 69 | if ggerr != nil { 70 | return nil, ggerr 71 | } 72 | 73 | log.WithFields(log.Fields{ 74 | "dir": ghostDir, 75 | }).Debug("ghost repo was cloned") 76 | 77 | return &WorkingEnv{ 78 | WorkingEnvSpec: weSpec, 79 | GhostDir: ghostDir, 80 | }, nil 81 | } 82 | 83 | func (weSpec WorkingEnv) Clean() errors.GitGhostError { 84 | return errors.WithStack(os.RemoveAll(weSpec.GhostDir)) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/util/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 19 | 20 | log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | // LogDeferredError calls a given function and log an error according to the result 24 | func LogDeferredError(f func() error) { 25 | err := f() 26 | if err != nil { 27 | log.Debugf("Error during defered call: %s", err) 28 | } 29 | } 30 | 31 | // LogDeferredGitGhostError calls a given function and log an GitGhostError according to the result 32 | func LogDeferredGitGhostError(f func() errors.GitGhostError) { 33 | err := f() 34 | if err != nil { 35 | log.Errorf("Error during defered call: %s", err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/util/errors/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package errors 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/pkg/errors" 21 | 22 | log "github.com/sirupsen/logrus" 23 | ) 24 | 25 | type GitGhostError interface { 26 | StackTrace() errors.StackTrace 27 | Error() string 28 | Cause() error 29 | } 30 | 31 | // LogErrorWithStack emits a log message with errors.GitGhostError level and stack trace with debug level 32 | func LogErrorWithStack(err GitGhostError) { 33 | var fields log.Fields 34 | if log.GetLevel() == log.TraceLevel { 35 | fields = log.Fields{"stacktrace": fmt.Sprintf("%+v", err)} 36 | } 37 | log.WithFields(fields).Error(err) 38 | } 39 | 40 | func Errorf(s string, args ...interface{}) GitGhostError { 41 | return errors.WithStack(fmt.Errorf(s, args...)).(GitGhostError) 42 | } 43 | 44 | func New(s string) GitGhostError { 45 | return errors.WithStack(fmt.Errorf(s)).(GitGhostError) 46 | } 47 | 48 | func WithStack(err error) GitGhostError { 49 | if err == nil { 50 | return nil 51 | } 52 | if gitghosterr, ok := err.(GitGhostError); ok { 53 | return gitghosterr 54 | } 55 | return errors.WithStack(err).(GitGhostError) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/util/errors/errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package errors_test 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestLogErrorWithStack(t *testing.T) { 27 | errors.LogErrorWithStack(errors.New("foo")) 28 | } 29 | 30 | func TestNew(t *testing.T) { 31 | err := errors.New("foo") 32 | assert.Equal(t, "foo", err.Error()) 33 | } 34 | 35 | func TestErrorf(t *testing.T) { 36 | err := errors.Errorf("%s", "foo") 37 | assert.Equal(t, "foo", err.Error()) 38 | } 39 | 40 | func TestWithStack(t *testing.T) { 41 | // for normal error 42 | err := errors.WithStack(fmt.Errorf("foo")) 43 | assert.Equal(t, "foo", err.Error()) 44 | // for GitGhostError 45 | err = errors.WithStack(errors.New("foo")) 46 | assert.Equal(t, "foo", err.Error()) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/util/exec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "bytes" 19 | "os" 20 | "os/exec" 21 | "strings" 22 | "syscall" 23 | 24 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 25 | 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | func JustOutputCmd(cmd *exec.Cmd) ([]byte, errors.GitGhostError) { 30 | wd, _ := os.Getwd() 31 | log.WithFields(log.Fields{ 32 | "pwd": wd, 33 | "command": strings.Join(cmd.Args, " "), 34 | }).Debug("exec") 35 | stderr := bytes.NewBufferString("") 36 | cmd.Stderr = stderr 37 | bytes, err := cmd.Output() 38 | if err != nil { 39 | s := stderr.String() 40 | if s != "" { 41 | return []byte{}, errors.New(s) 42 | } 43 | return []byte{}, errors.WithStack(err) 44 | } 45 | return bytes, nil 46 | } 47 | 48 | func JustRunCmd(cmd *exec.Cmd) errors.GitGhostError { 49 | wd, _ := os.Getwd() 50 | log.WithFields(log.Fields{ 51 | "pwd": wd, 52 | "command": strings.Join(cmd.Args, " "), 53 | }).Debug("exec") 54 | stderr := bytes.NewBufferString("") 55 | cmd.Stderr = stderr 56 | err := cmd.Run() 57 | if err != nil { 58 | s := stderr.String() 59 | if s != "" { 60 | return errors.New(s) 61 | } 62 | return errors.WithStack(err) 63 | } 64 | return nil 65 | } 66 | 67 | func GetExitCode(err error) int { 68 | if exiterr, ok := err.(*exec.ExitError); ok { 69 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 70 | return status.ExitStatus() 71 | } 72 | } 73 | return -1 74 | } 75 | -------------------------------------------------------------------------------- /pkg/util/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "os" 19 | "path/filepath" 20 | 21 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 22 | ) 23 | 24 | // FileSize returns file size of a given file 25 | func FileSize(filepath string) (int64, errors.GitGhostError) { 26 | fi, err := os.Stat(filepath) 27 | if err != nil { 28 | return 0, errors.WithStack(err) 29 | } 30 | 31 | return fi.Size(), nil 32 | } 33 | 34 | // WalkSymlink reads a symlink and call a given callback until the resolved path is not a symlink.WalkSymlink 35 | func WalkSymlink(dir, path string, cb func([]string, string) errors.GitGhostError) errors.GitGhostError { 36 | abspath := path 37 | if !filepath.IsAbs(path) { 38 | abspath = filepath.Clean(filepath.Join(dir, path)) 39 | } 40 | islink, err := IsSymlink(abspath) 41 | if err != nil { 42 | return err 43 | } 44 | if !islink { 45 | return errors.Errorf("%s is not a symlink", abspath) 46 | } 47 | 48 | resolved := abspath 49 | paths := []string{path} 50 | for { 51 | abspath = resolved 52 | if !filepath.IsAbs(resolved) { 53 | abspath = filepath.Clean(filepath.Join(dir, resolved)) 54 | } 55 | islink, ggerr := IsSymlink(abspath) 56 | if ggerr != nil { 57 | return ggerr 58 | } 59 | if !islink { 60 | break 61 | } 62 | path, err := os.Readlink(abspath) 63 | if err != nil { 64 | return errors.WithStack(err) 65 | } 66 | ggerr = cb(paths, path) 67 | if ggerr != nil { 68 | return errors.WithStack(ggerr) 69 | } 70 | resolved = path 71 | paths = append(paths, path) 72 | } 73 | return nil 74 | } 75 | 76 | // IsSymlink returns whether a given file is a directory 77 | func IsDir(path string) (bool, errors.GitGhostError) { 78 | stat, err := os.Stat(path) 79 | if err != nil { 80 | return false, errors.WithStack(err) 81 | } 82 | return stat.IsDir(), nil 83 | } 84 | 85 | // IsSymlink returns whether a given file is a symlink 86 | func IsSymlink(path string) (bool, errors.GitGhostError) { 87 | fi, err := os.Lstat(path) 88 | if err != nil { 89 | return false, errors.WithStack(err) 90 | } 91 | return fi.Mode()&os.ModeSymlink != 0, nil 92 | } 93 | -------------------------------------------------------------------------------- /pkg/util/hash/hash.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hash 16 | 17 | import ( 18 | "crypto/sha1" 19 | "fmt" 20 | "io" 21 | "os" 22 | 23 | "github.com/pfnet-research/git-ghost/pkg/util/errors" 24 | ) 25 | 26 | func GenerateFileContentHash(filepath string) (string, errors.GitGhostError) { 27 | // ref: https://pkg.go.dev/crypto/sha1#example-New-File 28 | f, err := os.Open(filepath) 29 | if err != nil { 30 | return "", errors.WithStack(err) 31 | } 32 | defer f.Close() 33 | h := sha1.New() 34 | if _, err := io.Copy(h, f); err != nil { 35 | return "", errors.WithStack(err) 36 | } 37 | return fmt.Sprintf("%x", h.Sum(nil)), nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/util/hash/hash_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hash_test 16 | 17 | import ( 18 | "os" 19 | "os/exec" 20 | "strings" 21 | "testing" 22 | 23 | "github.com/pfnet-research/git-ghost/pkg/util/hash" 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func calculateHashWithCommand(filepath string) (string, error) { 28 | cmd := exec.Command("sha1sum", "-b", filepath) 29 | output, err := cmd.Output() 30 | if err != nil { 31 | return "", err 32 | } 33 | hash := strings.Split(string(output), " ")[0] 34 | return hash, nil 35 | } 36 | 37 | // Check compatibility so that patches generated by previous versions of git-ghost can be loaded 38 | func TestHashCompatibility(t *testing.T) { 39 | tmpFile, err := os.CreateTemp(os.TempDir(), "tempfile-test-") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | oldHash, err := calculateHashWithCommand(tmpFile.Name()) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | newHash, err := hash.GenerateFileContentHash(tmpFile.Name()) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | assert.Equal(t, oldHash, newHash) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/util/logrus_fields.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "reflect" 19 | 20 | log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | func ToFields(structObj interface{}) (fields log.Fields) { 24 | fields = make(log.Fields) 25 | 26 | ptyp := reflect.TypeOf(structObj) // a reflect.Type 27 | pval := reflect.ValueOf(structObj) // a reflect.Value 28 | 29 | var typ reflect.Type 30 | var val reflect.Value 31 | if ptyp.Kind() == reflect.Ptr { 32 | typ = ptyp.Elem() 33 | val = pval.Elem() 34 | } else { 35 | typ = ptyp 36 | val = pval 37 | } 38 | for i := 0; i < typ.NumField(); i++ { 39 | name := typ.Field(i).Name 40 | value := val.FieldByName(name).Interface() 41 | fields[name] = value 42 | } 43 | 44 | return 45 | } 46 | 47 | func MergeFields(fieldss ...log.Fields) log.Fields { 48 | merged := make(log.Fields) 49 | for _, fields := range fieldss { 50 | for k, v := range fields { 51 | merged[k] = v 52 | } 53 | } 54 | return merged 55 | } 56 | -------------------------------------------------------------------------------- /pkg/util/slice.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | type empty struct{} 18 | 19 | func UniqueStringSlice(slice []string) []string { 20 | m := make(map[string]empty) 21 | 22 | for _, ele := range slice { 23 | m[ele] = empty{} 24 | } 25 | 26 | uniq := []string{} 27 | for i := range m { 28 | uniq = append(uniq, i) 29 | } 30 | 31 | return uniq 32 | } 33 | -------------------------------------------------------------------------------- /release.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye-slim 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get update -y && \ 4 | apt-get install -y --no-install-recommends \ 5 | git \ 6 | ca-certificates \ 7 | openssh-client \ 8 | && \ 9 | apt-get clean && \ 10 | rm -rf /var/lib/apt/lists/* 11 | COPY git-ghost /usr/local/bin/ 12 | -------------------------------------------------------------------------------- /scripts/license/add.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Preferred Networks, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Adds a license header to files. 17 | """ 18 | 19 | from pathlib import Path 20 | import subprocess 21 | 22 | from license_header import license_header, has_license_header 23 | 24 | PROJECT_ROOT = Path(subprocess.check_output( 25 | "git rev-parse --show-toplevel".split()).strip().decode("utf-8")) 26 | 27 | 28 | def main(verbose=False): 29 | add(PROJECT_ROOT.glob("*[!vendor]/**/*_k8s.go"), 30 | license_header("//", True), verbose) 31 | add([p for p in PROJECT_ROOT.glob("*[!vendor]/**/*.go") 32 | if p.name[-7:] != "_k8s.go"], license_header("//"), verbose) 33 | add(PROJECT_ROOT.glob("*[!vendor]/**/*.py"), license_header("#"), verbose) 34 | add(PROJECT_ROOT.glob("*[!vendor]/**/*.sh"), license_header("#"), verbose) 35 | 36 | 37 | def add(paths, license_header, verbose): 38 | for p in paths: 39 | if verbose: 40 | print("Checking", p.relative_to(PROJECT_ROOT)) 41 | 42 | if has_license_header(p, license_header): 43 | continue 44 | 45 | print("Add license header to file", p.relative_to(PROJECT_ROOT)) 46 | 47 | with p.open() as f: 48 | content = f.readlines() 49 | 50 | if content[0][:2] == "#!": # reserve shebangs 51 | content_new = [content[0], "\n", license_header, "\n"] 52 | 53 | # put a newline after the license header only if necessary 54 | if content[1] != "\n": 55 | content_new += ["\n"] 56 | content_new += content[1:] 57 | else: 58 | content_new = [license_header, "\n", "\n"] + content 59 | 60 | with p.open("w") as f: 61 | f.write(''.join(content_new)) 62 | 63 | 64 | if __name__ == "__main__": 65 | from argparse import ArgumentParser 66 | 67 | parser = ArgumentParser() 68 | parser.add_argument("-v", "--verbose", action="store_true") 69 | 70 | args = parser.parse_args() 71 | main(args.verbose) 72 | -------------------------------------------------------------------------------- /scripts/license/check.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Preferred Networks, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Checks whether files have an appropriate license header. 17 | """ 18 | 19 | from pathlib import Path 20 | import subprocess 21 | 22 | from license_header import license_header, has_license_header 23 | 24 | PROJECT_ROOT = Path(subprocess.check_output( 25 | "git rev-parse --show-toplevel".split()).strip().decode("utf-8")) 26 | 27 | 28 | def main(verbose=False): 29 | ok = True 30 | 31 | ok &= check( 32 | PROJECT_ROOT.glob("./*[!vendor]/**/*_k8s.go"), 33 | license_header("//", modification=True), verbose) 34 | ok &= check( 35 | [p for p in PROJECT_ROOT.glob( 36 | "./*[!vendor]/**/*.go") if p.name[-7:] != "_k8s.go"], 37 | license_header("//"), verbose) 38 | ok &= check( 39 | PROJECT_ROOT.glob("./*[!vendor]/**/*.py"), license_header("#"), verbose) 40 | ok &= check( 41 | PROJECT_ROOT.glob("./*[!vendor]/**/*.sh"), license_header("#"), verbose) 42 | 43 | return 0 if ok else 1 44 | 45 | 46 | def check(paths, license_header, verbose): 47 | ok = True 48 | 49 | for p in paths: 50 | if verbose: 51 | print("Checking", p.relative_to(PROJECT_ROOT)) 52 | 53 | if not p.exists(): 54 | print("File", p.relative_to(PROJECT_ROOT), "does not exist") 55 | ok = False 56 | continue 57 | 58 | if not has_license_header(p, license_header): 59 | print(p.relative_to(PROJECT_ROOT), "is missing a license header") 60 | ok = False 61 | 62 | return ok 63 | 64 | 65 | if __name__ == "__main__": 66 | from argparse import ArgumentParser 67 | 68 | parser = ArgumentParser() 69 | parser.add_argument("-v", "--verbose", action="store_true") 70 | 71 | args = parser.parse_args() 72 | exit(main(args.verbose)) 73 | -------------------------------------------------------------------------------- /scripts/license/license_header.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Preferred Networks, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | LICENSE_HEADER = [ 16 | 'Copyright 2019 Preferred Networks, Inc.', 17 | '', 18 | 'Licensed under the Apache License, Version 2.0 (the "License");', 19 | 'you may not use this file except in compliance with the License.', 20 | 'You may obtain a copy of the License at', 21 | '', 22 | ' http://www.apache.org/licenses/LICENSE-2.0', 23 | '', 24 | 'Unless required by applicable law or agreed to in writing, software', 25 | 'distributed under the License is distributed on an "AS IS" BASIS,', 26 | 'WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.', 27 | 'See the License for the specific language governing permissions and', 28 | 'limitations under the License.', 29 | ] 30 | 31 | LICENSE_HEADER_MODIFICATION = [ 32 | 'Modifications copyright 2019 Preferred Networks, Inc.', 33 | ] + LICENSE_HEADER[1:] 34 | 35 | 36 | def license_header(comment, modification=False): 37 | """ 38 | Returns a license header. 39 | Each line is preceded by `comment` string, and a single space if the line is not empty. 40 | """ 41 | 42 | license = LICENSE_HEADER_MODIFICATION if modification else LICENSE_HEADER 43 | return '\n'.join( 44 | "{} {}".format(comment, l) if len(l) > 0 else comment 45 | for l in license 46 | ) 47 | 48 | 49 | def has_license_header(path, license_header): 50 | """ 51 | Returns whether the file at `path` contains `license_header` string. 52 | """ 53 | 54 | return license_header in path.open().read() 55 | -------------------------------------------------------------------------------- /test/e2e/e2e.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e2e 16 | -------------------------------------------------------------------------------- /test/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | // +build !no_e2e 2 | 3 | // Copyright 2019 Preferred Networks, Inc. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/pfnet-research/git-ghost/test/util" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | var ( 31 | ghostDir *util.WorkDir 32 | ) 33 | 34 | func setup() error { 35 | dir, err := util.CreateGitWorkDir() 36 | if err != nil { 37 | return err 38 | } 39 | ghostDir = dir 40 | return nil 41 | } 42 | 43 | func teardown() error { 44 | if ghostDir != nil { 45 | defer ghostDir.Remove() 46 | } 47 | return nil 48 | } 49 | 50 | func TestMain(m *testing.M) { 51 | err := setup() 52 | if err != nil { 53 | teardown() 54 | panic(err) 55 | } 56 | 57 | result := m.Run() 58 | 59 | err = teardown() 60 | if err != nil { 61 | fmt.Printf("%s\n", err) 62 | } 63 | 64 | os.Exit(result) 65 | } 66 | 67 | func TestTypeDefault(t *testing.T) { 68 | srcDir, dstDir, err := setupBasicEnv(ghostDir) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | defer srcDir.Remove() 73 | defer dstDir.Remove() 74 | 75 | // Make one modification 76 | _, _, err = srcDir.RunCommmand("bash", "-c", "echo c > sample.txt") 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | stdout, _, err := srcDir.RunCommmand("git", "rev-parse", "HEAD") 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | baseCommit := strings.TrimRight(stdout, "\n") 86 | 87 | stdout, _, err = srcDir.RunGitGhostCommmand("push") 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | hashes := strings.Split(strings.TrimRight(stdout, "\n"), " ") 92 | assert.Equal(t, 2, len(hashes)) 93 | diffBaseCommit := hashes[0] 94 | diffHash := hashes[1] 95 | assert.NotEqual(t, "", diffBaseCommit) 96 | assert.NotEqual(t, "", diffHash) 97 | 98 | stdout, _, err = srcDir.RunGitGhostCommmand("show", diffHash) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | assert.Contains(t, stdout, "-b\n+c\n") 103 | 104 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | assert.Equal(t, "b\n", stdout) 109 | _, _, err = dstDir.RunGitGhostCommmand("pull", diffHash) 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | assert.Equal(t, "c\n", stdout) 118 | 119 | stdout, _, err = dstDir.RunGitGhostCommmand("list") 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, diffHash)) 124 | 125 | stdout, _, err = dstDir.RunGitGhostCommmand("delete", "--all") 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, diffHash)) 130 | 131 | stdout, _, err = dstDir.RunGitGhostCommmand("list") 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | assert.NotContains(t, stdout, fmt.Sprintf("%s %s", baseCommit, diffHash)) 136 | } 137 | 138 | func TestTypeCommits(t *testing.T) { 139 | srcDir, dstDir, err := setupBasicEnv(ghostDir) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | defer srcDir.Remove() 144 | defer dstDir.Remove() 145 | 146 | stdout, _, err := srcDir.RunGitGhostCommmand("push", "commits", "HEAD~1") 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | hashes := strings.Split(stdout, " ") 151 | assert.Equal(t, 2, len(hashes)) 152 | baseCommit := hashes[0] 153 | targetCommit := hashes[1] 154 | assert.NotEqual(t, "", baseCommit) 155 | assert.NotEqual(t, "", targetCommit) 156 | 157 | stdout, _, err = srcDir.RunGitGhostCommmand("show", "commits", baseCommit, targetCommit) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | assert.Contains(t, stdout, "-a\n+b\n") 162 | 163 | _, _, err = dstDir.RunCommmand("git", "checkout", baseCommit) 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | assert.Equal(t, "a\n", stdout) 172 | _, _, err = dstDir.RunGitGhostCommmand("pull", "commits", baseCommit, targetCommit) 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 177 | if err != nil { 178 | t.Fatal(err) 179 | } 180 | assert.Equal(t, "b\n", stdout) 181 | 182 | stdout, _, err = dstDir.RunGitGhostCommmand("list", "commits") 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 187 | 188 | stdout, _, err = dstDir.RunGitGhostCommmand("delete", "commits", "--all") 189 | if err != nil { 190 | t.Fatal(err) 191 | } 192 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 193 | 194 | stdout, _, err = dstDir.RunGitGhostCommmand("list", "commits") 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | assert.NotContains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 199 | } 200 | 201 | func TestTypeDiff(t *testing.T) { 202 | srcDir, dstDir, err := setupBasicEnv(ghostDir) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | defer srcDir.Remove() 207 | defer dstDir.Remove() 208 | 209 | // Make one modification 210 | _, _, err = srcDir.RunCommmand("bash", "-c", "echo c > sample.txt") 211 | if err != nil { 212 | t.Fatal(err) 213 | } 214 | 215 | stdout, _, err := srcDir.RunCommmand("git", "rev-parse", "HEAD") 216 | if err != nil { 217 | t.Fatal(err) 218 | } 219 | baseCommit := strings.TrimRight(stdout, "\n") 220 | 221 | stdout, _, err = srcDir.RunGitGhostCommmand("push", "diff") 222 | if err != nil { 223 | t.Fatal(err) 224 | } 225 | hashes := strings.Split(strings.TrimRight(stdout, "\n"), " ") 226 | assert.Equal(t, 2, len(hashes)) 227 | diffBaseCommit := hashes[0] 228 | diffHash := hashes[1] 229 | assert.NotEqual(t, "", diffBaseCommit) 230 | assert.NotEqual(t, "", diffHash) 231 | 232 | stdout, _, err = srcDir.RunGitGhostCommmand("show", "diff", diffHash) 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | assert.Contains(t, stdout, "-b\n+c\n") 237 | 238 | _, _, err = dstDir.RunGitGhostCommmand("pull", "diff", diffHash) 239 | if err != nil { 240 | t.Fatal(err) 241 | } 242 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 243 | if err != nil { 244 | t.Fatal(err) 245 | } 246 | assert.Equal(t, "c\n", stdout) 247 | 248 | stdout, _, err = dstDir.RunGitGhostCommmand("list", "diff") 249 | if err != nil { 250 | t.Fatal(err) 251 | } 252 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, diffHash)) 253 | 254 | stdout, _, err = dstDir.RunGitGhostCommmand("delete", "diff", "--all") 255 | if err != nil { 256 | t.Fatal(err) 257 | } 258 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, diffHash)) 259 | 260 | stdout, _, err = dstDir.RunGitGhostCommmand("list", "diff") 261 | if err != nil { 262 | t.Fatal(err) 263 | } 264 | assert.NotContains(t, stdout, fmt.Sprintf("%s %s", baseCommit, diffHash)) 265 | } 266 | 267 | func TestTypeAll(t *testing.T) { 268 | srcDir, dstDir, err := setupBasicEnv(ghostDir) 269 | if err != nil { 270 | t.Fatal(err) 271 | } 272 | defer srcDir.Remove() 273 | defer dstDir.Remove() 274 | 275 | // Make one modification 276 | _, _, err = srcDir.RunCommmand("bash", "-c", "echo c > sample.txt") 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | 281 | stdout, _, err := srcDir.RunGitGhostCommmand("push", "all", "HEAD~1") 282 | if err != nil { 283 | t.Fatal(err) 284 | } 285 | lines := strings.Split(stdout, "\n") 286 | hashes := strings.Split(lines[0], " ") 287 | assert.Equal(t, 2, len(hashes)) 288 | baseCommit := hashes[0] 289 | targetCommit := hashes[1] 290 | assert.NotEqual(t, "", baseCommit) 291 | assert.NotEqual(t, "", targetCommit) 292 | 293 | hashes = strings.Split(lines[1], " ") 294 | assert.Equal(t, 2, len(hashes)) 295 | diffBaseCommit := hashes[0] 296 | diffHash := hashes[1] 297 | assert.NotEqual(t, "", diffBaseCommit) 298 | assert.NotEqual(t, "", diffHash) 299 | 300 | stdout, _, err = srcDir.RunGitGhostCommmand("show", "all", baseCommit, targetCommit, diffHash) 301 | if err != nil { 302 | t.Fatal(err) 303 | } 304 | assert.Contains(t, stdout, "-a\n+b\n") 305 | assert.Contains(t, stdout, "-b\n+c\n") 306 | 307 | _, _, err = dstDir.RunCommmand("git", "checkout", baseCommit) 308 | if err != nil { 309 | t.Fatal(err) 310 | } 311 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 312 | if err != nil { 313 | t.Fatal(err) 314 | } 315 | assert.Equal(t, "a\n", stdout) 316 | _, _, err = dstDir.RunGitGhostCommmand("pull", "all", baseCommit, targetCommit, diffHash) 317 | if err != nil { 318 | t.Fatal(err) 319 | } 320 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 321 | if err != nil { 322 | t.Fatal(err) 323 | } 324 | assert.Equal(t, "c\n", stdout) 325 | 326 | stdout, _, err = dstDir.RunGitGhostCommmand("list", "all") 327 | if err != nil { 328 | t.Fatal(err) 329 | } 330 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 331 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", targetCommit, diffHash)) 332 | 333 | stdout, _, err = dstDir.RunGitGhostCommmand("delete", "all", "-v", "--all") 334 | if err != nil { 335 | t.Fatal(err) 336 | } 337 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 338 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", targetCommit, diffHash)) 339 | 340 | stdout, _, err = dstDir.RunGitGhostCommmand("list", "all") 341 | if err != nil { 342 | t.Fatal(err) 343 | } 344 | assert.NotContains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 345 | assert.NotContains(t, stdout, fmt.Sprintf("%s %s", targetCommit, diffHash)) 346 | } 347 | 348 | func TestEmptyCommitsAndDiff(t *testing.T) { 349 | srcDir, dstDir, err := setupBasicEnv(ghostDir) 350 | if err != nil { 351 | t.Fatal(err) 352 | } 353 | defer srcDir.Remove() 354 | defer dstDir.Remove() 355 | 356 | stdout, _, err := srcDir.RunGitGhostCommmand("push", "all", "HEAD", "HEAD") 357 | if err != nil { 358 | t.Fatal(err) 359 | } 360 | lines := strings.Split(stdout, "\n") 361 | hashes := strings.Split(lines[0], " ") 362 | assert.Equal(t, 2, len(hashes)) 363 | baseCommit := hashes[0] 364 | targetCommit := hashes[1] 365 | assert.NotEqual(t, "", baseCommit) 366 | assert.NotEqual(t, "", targetCommit) 367 | 368 | hashes = strings.Split(lines[1], " ") 369 | assert.Equal(t, 2, len(hashes)) 370 | diffBaseCommit := hashes[0] 371 | diffHash := hashes[1] 372 | assert.NotEqual(t, "", diffBaseCommit) 373 | assert.NotEqual(t, "", diffHash) 374 | 375 | stdout, _, err = srcDir.RunGitGhostCommmand("show", "all", baseCommit, targetCommit, diffHash) 376 | if err != nil { 377 | t.Fatal(err) 378 | } 379 | assert.Equal(t, "", stdout) 380 | 381 | _, _, err = dstDir.RunCommmand("git", "checkout", baseCommit) 382 | if err != nil { 383 | t.Fatal(err) 384 | } 385 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 386 | if err != nil { 387 | t.Fatal(err) 388 | } 389 | assert.Equal(t, "b\n", stdout) 390 | _, _, err = dstDir.RunGitGhostCommmand("pull", "all", baseCommit, targetCommit, diffHash) 391 | if err != nil { 392 | t.Fatal(err) 393 | } 394 | stdout, _, err = dstDir.RunCommmand("cat", "sample.txt") 395 | if err != nil { 396 | t.Fatal(err) 397 | } 398 | assert.Equal(t, "b\n", stdout) 399 | 400 | stdout, _, err = dstDir.RunGitGhostCommmand("list", "all") 401 | if err != nil { 402 | t.Fatal(err) 403 | } 404 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 405 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", targetCommit, diffHash)) 406 | 407 | stdout, _, err = dstDir.RunGitGhostCommmand("delete", "all", "-v", "--all") 408 | if err != nil { 409 | t.Fatal(err) 410 | } 411 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 412 | assert.Contains(t, stdout, fmt.Sprintf("%s %s", targetCommit, diffHash)) 413 | 414 | stdout, _, err = dstDir.RunGitGhostCommmand("list", "all") 415 | if err != nil { 416 | t.Fatal(err) 417 | } 418 | assert.NotContains(t, stdout, fmt.Sprintf("%s %s", baseCommit, targetCommit)) 419 | assert.NotContains(t, stdout, fmt.Sprintf("%s %s", targetCommit, diffHash)) 420 | } 421 | 422 | func TestIncludeFile(t *testing.T) { 423 | srcDir, dstDir, err := setupBasicEnv(ghostDir) 424 | if err != nil { 425 | t.Fatal(err) 426 | } 427 | defer srcDir.Remove() 428 | defer dstDir.Remove() 429 | 430 | // Make one modification 431 | _, _, err = srcDir.RunCommmand("bash", "-c", "echo c > sample.txt") 432 | if err != nil { 433 | t.Fatal(err) 434 | } 435 | 436 | // Make a file 437 | _, _, err = srcDir.RunCommmand("bash", "-c", "echo 'this is an included file' > included_file") 438 | if err != nil { 439 | t.Fatal(err) 440 | } 441 | 442 | stdout, _, err := srcDir.RunGitGhostCommmand("push", "--include", "included_file") 443 | if err != nil { 444 | t.Fatal(err) 445 | } 446 | hashes := strings.Split(strings.TrimRight(stdout, "\n"), " ") 447 | assert.Equal(t, 2, len(hashes)) 448 | diffBaseCommit := hashes[0] 449 | diffHash := hashes[1] 450 | assert.NotEqual(t, "", diffBaseCommit) 451 | assert.NotEqual(t, "", diffHash) 452 | 453 | _, _, err = dstDir.RunGitGhostCommmand("pull", diffHash) 454 | if err != nil { 455 | t.Fatal(err) 456 | } 457 | stdout, _, err = dstDir.RunCommmand("cat", "included_file") 458 | if err != nil { 459 | t.Fatal(err) 460 | } 461 | assert.Equal(t, "this is an included file\n", stdout) 462 | } 463 | 464 | func TestIncludeLinkFile(t *testing.T) { 465 | srcDir, dstDir, err := setupBasicEnv(ghostDir) 466 | if err != nil { 467 | t.Fatal(err) 468 | } 469 | defer srcDir.Remove() 470 | defer dstDir.Remove() 471 | 472 | // Make one modification 473 | _, _, err = srcDir.RunCommmand("bash", "-c", "echo c > sample.txt") 474 | if err != nil { 475 | t.Fatal(err) 476 | } 477 | 478 | // Make a file 479 | _, _, err = srcDir.RunCommmand("bash", "-c", "echo 'this is an included file' > included_file") 480 | if err != nil { 481 | t.Fatal(err) 482 | } 483 | 484 | // Make a symlink to the file above 485 | _, _, err = srcDir.RunCommmand("bash", "-c", "ln -ns included_file included_link") 486 | if err != nil { 487 | t.Fatal(err) 488 | } 489 | 490 | stdout, _, err := srcDir.RunGitGhostCommmand("push", "--include", "included_link", "--follow-symlinks") 491 | if err != nil { 492 | t.Fatal(err) 493 | } 494 | hashes := strings.Split(strings.TrimRight(stdout, "\n"), " ") 495 | assert.Equal(t, 2, len(hashes)) 496 | diffBaseCommit := hashes[0] 497 | diffHash := hashes[1] 498 | assert.NotEqual(t, "", diffBaseCommit) 499 | assert.NotEqual(t, "", diffHash) 500 | 501 | _, _, err = dstDir.RunGitGhostCommmand("pull", diffHash) 502 | if err != nil { 503 | t.Fatal(err) 504 | } 505 | stdout, _, err = dstDir.RunCommmand("cat", "included_file") 506 | if err != nil { 507 | t.Fatal(err) 508 | } 509 | assert.Equal(t, "this is an included file\n", stdout) 510 | stdout, _, err = dstDir.RunCommmand("cat", "included_link") 511 | if err != nil { 512 | t.Fatal(err) 513 | } 514 | assert.Equal(t, "this is an included file\n", stdout) 515 | } 516 | 517 | func setupBasicEnv(workDir *util.WorkDir) (*util.WorkDir, *util.WorkDir, error) { 518 | env := map[string]string{ 519 | "GIT_GHOST_REPO": workDir.Dir, 520 | } 521 | 522 | srcDir, err := util.CreateGitWorkDir() 523 | if err != nil { 524 | return nil, nil, err 525 | } 526 | 527 | err = setupBasicGitRepo(srcDir) 528 | if err != nil { 529 | srcDir.Remove() 530 | return nil, nil, err 531 | } 532 | srcDir.Env = env 533 | 534 | dstDir, err := util.CloneWorkDir(srcDir) 535 | if err != nil { 536 | srcDir.Remove() 537 | return nil, nil, err 538 | } 539 | dstDir.Env = env 540 | 541 | return srcDir, dstDir, nil 542 | } 543 | 544 | func setupBasicGitRepo(wd *util.WorkDir) error { 545 | var err error 546 | _, _, err = wd.RunCommmand("bash", "-c", "echo a > sample.txt") 547 | if err != nil { 548 | return err 549 | } 550 | _, _, err = wd.RunCommmand("git", "add", "sample.txt") 551 | if err != nil { 552 | return err 553 | } 554 | _, _, err = wd.RunCommmand("git", "commit", "sample.txt", "-m", "initial commit") 555 | if err != nil { 556 | return err 557 | } 558 | _, _, err = wd.RunCommmand("bash", "-c", "echo b > sample.txt") 559 | if err != nil { 560 | return err 561 | } 562 | _, _, err = wd.RunCommmand("git", "commit", "sample.txt", "-m", "second commit") 563 | if err != nil { 564 | return err 565 | } 566 | return nil 567 | } 568 | -------------------------------------------------------------------------------- /test/util/workdir.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Preferred Networks, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os" 21 | "os/exec" 22 | ) 23 | 24 | type WorkDir struct { 25 | Dir string 26 | Env map[string]string 27 | } 28 | 29 | type CommandError struct { 30 | InternalError error 31 | Stdout string 32 | Stderr string 33 | } 34 | 35 | func (ce *CommandError) Error() string { 36 | return fmt.Sprintf("%s\n\nstdout:\n%s\n\nstderr:\n%s", ce.InternalError, ce.Stdout, ce.Stderr) 37 | } 38 | 39 | func CloneWorkDir(base *WorkDir) (*WorkDir, error) { 40 | wd, err := CreateWorkDir() 41 | if err != nil { 42 | return nil, err 43 | } 44 | _, _, err = wd.RunCommmand("git", "clone", base.Dir, wd.Dir) 45 | if err != nil { 46 | _ = wd.Remove() 47 | return nil, err 48 | } 49 | _, _, err = wd.RunCommmand("git", "config", "user.email", "you@example.com") 50 | if err != nil { 51 | _ = wd.Remove() 52 | return nil, err 53 | } 54 | _, _, err = wd.RunCommmand("git", "config", "user.name", "\"Your Name\"") 55 | if err != nil { 56 | _ = wd.Remove() 57 | return nil, err 58 | } 59 | return wd, nil 60 | } 61 | 62 | func CreateGitWorkDir() (*WorkDir, error) { 63 | wd, err := CreateWorkDir() 64 | if err != nil { 65 | return nil, err 66 | } 67 | _, _, err = wd.RunCommmand("git", "init") 68 | if err != nil { 69 | _ = wd.Remove() 70 | return nil, err 71 | } 72 | _, _, err = wd.RunCommmand("git", "config", "user.email", "you@example.com") 73 | if err != nil { 74 | _ = wd.Remove() 75 | return nil, err 76 | } 77 | _, _, err = wd.RunCommmand("git", "config", "user.name", "\"Your Name\"") 78 | if err != nil { 79 | _ = wd.Remove() 80 | return nil, err 81 | } 82 | return wd, nil 83 | } 84 | 85 | func CreateWorkDir() (*WorkDir, error) { 86 | dir, err := os.MkdirTemp("", "git-ghost-e2e-test-") 87 | if err != nil { 88 | return nil, err 89 | } 90 | return &WorkDir{Dir: dir}, nil 91 | } 92 | 93 | func (wd *WorkDir) Remove() error { 94 | return os.RemoveAll(wd.Dir) 95 | } 96 | 97 | func (wd *WorkDir) RunGitGhostCommmand(args ...string) (string, string, error) { 98 | newArgs := []string{"ghost"} 99 | debug := os.Getenv("DEBUG") 100 | if debug != "" { 101 | newArgs = append(newArgs, "-vvv") 102 | } 103 | newArgs = append(newArgs, args...) 104 | return wd.RunCommmand("git", newArgs...) 105 | } 106 | 107 | func (wd *WorkDir) RunCommmand(command string, args ...string) (string, string, error) { 108 | cmd := exec.Command(command, args...) 109 | stdout := bytes.NewBufferString("") 110 | stderr := bytes.NewBufferString("") 111 | cmd.Dir = wd.Dir 112 | env := make([]string, 0, len(os.Environ())+len(wd.Env)+1) 113 | env = append(env, os.Environ()...) 114 | for key, val := range wd.Env { 115 | env = append(env, fmt.Sprintf("%s=%s", key, val)) 116 | } 117 | cmd.Env = env 118 | cmd.Stdout = stdout 119 | cmd.Stderr = stderr 120 | err := cmd.Run() 121 | if err != nil { 122 | err = &CommandError{ 123 | InternalError: err, 124 | Stdout: stdout.String(), 125 | Stderr: stderr.String(), 126 | } 127 | } 128 | return stdout.String(), stderr.String(), err 129 | } 130 | --------------------------------------------------------------------------------