forked from git-lfs/git-lfs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Sometimes users have text files that they'd like to be able perform merges on that they also want to store in Git LFS. Let's add a custom merge driver that users can use to merge these text files, either with Git, or with a custom merge tool like the one shipped with Unity.
- Loading branch information
Showing
5 changed files
with
528 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
package commands | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
|
||
"github.com/git-lfs/git-lfs/v3/errors" | ||
"github.com/git-lfs/git-lfs/v3/lfs" | ||
"github.com/git-lfs/git-lfs/v3/subprocess" | ||
"github.com/git-lfs/git-lfs/v3/tr" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var ( | ||
mergeDriverAncestor string | ||
mergeDriverCurrent string | ||
mergeDriverOther string | ||
mergeDriverOutput string | ||
mergeDriverProgram string | ||
mergeDriverMarkerSize int | ||
) | ||
|
||
func mergeDriverCommand(cmd *cobra.Command, args []string) { | ||
if len(mergeDriverAncestor) == 0 || len(mergeDriverCurrent) == 0 || len(mergeDriverOther) == 0 || len(mergeDriverOutput) == 0 { | ||
Exit(tr.Tr.Get("the --ancestor, --current, --other, and --output options are mandatory")) | ||
} | ||
|
||
fileSpecifiers := make(map[string]string) | ||
gf := lfs.NewGitFilter(cfg) | ||
mergeProcessInput(gf, mergeDriverAncestor, fileSpecifiers, "O") | ||
mergeProcessInput(gf, mergeDriverCurrent, fileSpecifiers, "A") | ||
mergeProcessInput(gf, mergeDriverOther, fileSpecifiers, "B") | ||
mergeProcessInput(gf, "", fileSpecifiers, "D") | ||
|
||
fileSpecifiers["L"] = fmt.Sprintf("%d", mergeDriverMarkerSize) | ||
|
||
if len(mergeDriverProgram) == 0 { | ||
mergeDriverProgram = "git merge-file --stdout --marker-size=%L %A %O %B >%D" | ||
} | ||
|
||
status, err := processFiles(fileSpecifiers, mergeDriverProgram, mergeDriverOutput) | ||
if err != nil { | ||
ExitWithError(err) | ||
} | ||
os.Exit(status) | ||
} | ||
|
||
func processFiles(fileSpecifiers map[string]string, program string, outputFile string) (int, error) { | ||
defer mergeCleanup(fileSpecifiers) | ||
|
||
var exitStatus int | ||
formattedMergeProgram := subprocess.FormatPercentSequences(mergeDriverProgram, fileSpecifiers) | ||
cmd, err := subprocess.ExecCommand("sh", "-c", formattedMergeProgram) | ||
if err != nil { | ||
return -1, errors.New(tr.Tr.Get("failed to run merge program %q: %s", formattedMergeProgram, err)) | ||
} | ||
err = cmd.Run() | ||
// If it runs but exits nonzero, then that means there's conflicts | ||
if err != nil { | ||
if exitError, ok := err.(*exec.ExitError); ok { | ||
exitStatus = exitError.ProcessState.ExitCode() | ||
} else { | ||
return -1, errors.New(tr.Tr.Get("failed to run merge program %q: %s", formattedMergeProgram, err)) | ||
} | ||
} | ||
|
||
outputFp, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE, 0600) | ||
if err != nil { | ||
return -1, err | ||
} | ||
defer outputFp.Close() | ||
|
||
filename := fileSpecifiers["D"] | ||
|
||
stat, err := os.Stat(filename) | ||
if err != nil { | ||
return -1, err | ||
} | ||
|
||
inputFp, err := os.OpenFile(filename, os.O_RDONLY|os.O_CREATE, 0600) | ||
if err != nil { | ||
return -1, err | ||
} | ||
defer inputFp.Close() | ||
|
||
gf := lfs.NewGitFilter(cfg) | ||
_, err = clean(gf, outputFp, inputFp, filename, stat.Size()) | ||
if err != nil { | ||
return -1, err | ||
} | ||
|
||
return exitStatus, nil | ||
} | ||
|
||
func mergeCleanup(fileSpecifiers map[string]string) { | ||
ids := []string{"A", "O", "B", "D"} | ||
for _, id := range ids { | ||
os.Remove(fileSpecifiers[id]) | ||
} | ||
} | ||
|
||
func mergeProcessInput(gf *lfs.GitFilter, filename string, fileSpecifiers map[string]string, tag string) { | ||
file, err := lfs.TempFile(cfg, fmt.Sprintf("merge-driver-%s", tag)) | ||
if err != nil { | ||
Exit(tr.Tr.Get("could not create temporary file when merging: %s", err)) | ||
} | ||
defer file.Close() | ||
fileSpecifiers[tag] = file.Name() | ||
|
||
if len(filename) == 0 { | ||
return | ||
} | ||
|
||
pointer, err := lfs.DecodePointerFromFile(filename) | ||
if err != nil { | ||
if errors.IsNotAPointerError(err) { | ||
file.Close() | ||
if err := lfs.CopyFileContents(cfg, filename, file.Name()); err != nil { | ||
os.Remove(file.Name()) | ||
Exit(tr.Tr.Get("could not copy non-LFS content when merging: %s", err)) | ||
} | ||
return | ||
} else { | ||
os.Remove(file.Name()) | ||
Exit(tr.Tr.Get("could not decode pointer when merging: %s", err)) | ||
} | ||
} | ||
cb, fp, err := gf.CopyCallbackFile("download", file.Name(), 1, 1) | ||
if err != nil { | ||
os.Remove(file.Name()) | ||
Exit(tr.Tr.Get("could not create callback: %s", err)) | ||
} | ||
defer fp.Close() | ||
_, err = gf.Smudge(file, pointer, file.Name(), true, getTransferManifestOperationRemote("download", cfg.Remote()), cb) | ||
} | ||
|
||
func init() { | ||
RegisterCommand("merge-driver", mergeDriverCommand, func(cmd *cobra.Command) { | ||
cmd.Flags().StringVarP(&mergeDriverAncestor, "ancestor", "", "", "file with the ancestor version") | ||
cmd.Flags().StringVarP(&mergeDriverCurrent, "current", "", "", "file with the current version") | ||
cmd.Flags().StringVarP(&mergeDriverOther, "other", "", "", "file with the other version") | ||
cmd.Flags().StringVarP(&mergeDriverOutput, "output", "", "", "file with the output version") | ||
cmd.Flags().StringVarP(&mergeDriverProgram, "program", "", "", "program to run to perform the merge") | ||
cmd.Flags().IntVarP(&mergeDriverMarkerSize, "marker-size", "", 12, "merge marker size") | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
git-lfs-merge-driver(1) -- Merge text-based LFS files | ||
============================================================== | ||
|
||
## SYNOPSIS | ||
|
||
`git lfs merge-driver` [options] | ||
|
||
## DESCRIPTION | ||
|
||
Merge text files stored in Git LFS using the default Git merge machinery, or a | ||
custom merge driver if specified. Note that this, in general, does not support | ||
partial renames or copies because Git does not support them in this case. | ||
|
||
This program is intended to be invoked automatically by Git and not by users | ||
manually. See [CONFIGURATION] for details on the configuration required for | ||
that. | ||
|
||
## OPTIONS | ||
|
||
* `--ancestor` <path> | ||
Specify the file containing the ancestor revision. | ||
|
||
* `--current` <path> | ||
Specify the file containing the current revision. | ||
|
||
* `--marker-size` <num> | ||
Specify the conflict marker size as an integer. | ||
|
||
* `--other` <path> | ||
Specify the file containing the other revision. | ||
|
||
* `--program` <program> | ||
Specify a command, which is passed to the shell after substitution, that | ||
performs the actual merge. If this is not specified, `git merge-file` is | ||
invoked with appropriate arguments to perform the merge of the file. | ||
|
||
See [CONFIGURATION] for the sequences which are substituted here. | ||
|
||
## CONFIGURATION | ||
|
||
Git allows the use of a custom merge driver for files based on the `merge` | ||
attribute set in `.gitattributes`. By default, when using `git lfs track`, this | ||
value is set to `lfs`. | ||
|
||
Because Git LFS can be used to store both text and binary files and it isn't | ||
always clear which behavior should be used, Git LFS does not enable this merge | ||
driver by default. However, if you know that some or all of your files are text | ||
files, then you can set the `merge` attribute for those files to `lfs-text` and | ||
use `git config` to set the merge driver like so: | ||
|
||
```console | ||
$ git config merge.lfs-text.driver 'git lfs merge-driver --ancestor %O --current %A --other %B --marker-size %L --output %A' | ||
``` | ||
|
||
This tells Git to invoke the custom Git LFS merge driver, which in turn uses | ||
Git's merge machinery, to merge files where the `merge` attribute is set to | ||
`lfs-text`. Note that `lfs-text` here is an example and any syntactically valid | ||
value can be used. | ||
|
||
If you are using a special type of file that needs rules different from Git's | ||
standard merge machinery, you can also specify the `--program` option, which | ||
is passed to `sh` after substituting its own percent-encoded escapes: | ||
|
||
* `%A`: the current version | ||
* `%B`: the other version | ||
* `%D`: the destination version | ||
* `%O`: the ancestor version | ||
* `%L`: the conflict marker size | ||
|
||
Note that the percent sign must typically be doubled to prevent Git from | ||
substituting its own values here. Therefore, specifying the default behavior | ||
explicitly looks like this: | ||
|
||
```console | ||
$ git config merge.lfs-text.driver \ | ||
'git lfs merge-driver --ancestor %O --current %A --other %B --marker-size %L --output %A --program '\''git merge-file --stdout --marker-size=%%L %%A %%O %%B >%%D'\''' | ||
``` | ||
|
||
The exit status from the custom command should be zero on success or non-zero on | ||
conflicts or other failure. | ||
|
||
Note that if no merge driver is specified for the value of the `merge` attribute | ||
(as is the case by default with `merge=lfs`), then the default Git merge | ||
strategy is used. For LFS files, this means that Git will try to merge the | ||
pointer files, which usually is not useful. | ||
|
||
## SEE ALSO | ||
|
||
git-merge(1), git-merge-file(1), gitattributes(5) | ||
|
||
Part of the git-lfs(1) suite. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.