Skip to content

Commit cf7f967

Browse files
committed
commands: add flags to get conflicted contents
When there's a conflict with a file in Git LFS, it's difficult to get the LFS contents of the conflicted files so that they can be run through an appropriate diff tool. To make this easier, teach git lfs checkout the --base, --theirs, and --ours flags to check out the base, theirs, and ours outputs to a given path (specified with --to). Be sure not to print a deprecation message in this case, since this is not a deprecated use. Note that we use three different variables for the base, theirs, and ours flags because Cobra doesn't offer a command mode option that can parse all of the flags into one variable.
1 parent c0a6808 commit cf7f967

File tree

3 files changed

+135
-4
lines changed

3 files changed

+135
-4
lines changed

commands/command_checkout.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,28 @@ import (
1313
"github.com/spf13/cobra"
1414
)
1515

16+
var (
17+
checkoutTo string
18+
checkoutBase bool
19+
checkoutOurs bool
20+
checkoutTheirs bool
21+
)
22+
1623
func checkoutCommand(cmd *cobra.Command, args []string) {
1724
requireInRepo()
1825

26+
stage, err := whichCheckout()
27+
if err != nil {
28+
Exit("Error parsing args: %v", err)
29+
}
30+
31+
if checkoutTo != "" && stage != git.IndexStageDefault {
32+
checkoutConflict(rootedPaths(args)[0], stage)
33+
return
34+
} else if checkoutTo != "" || stage != git.IndexStageDefault {
35+
Exit("--to and exactly one of --theirs, --ours, and --base must be used together")
36+
}
37+
1938
msg := []string{
2039
"WARNING: 'git lfs checkout' is deprecated and will be removed in v3.0.0.",
2140

@@ -75,6 +94,63 @@ func checkoutCommand(cmd *cobra.Command, args []string) {
7594
singleCheckout.Close()
7695
}
7796

97+
func checkoutConflict(file string, stage git.IndexStage) {
98+
singleCheckout := newSingleCheckout(cfg.Git, "")
99+
if singleCheckout.Skip() {
100+
fmt.Println("Cannot checkout LFS objects, Git LFS is not installed.")
101+
return
102+
}
103+
104+
ref, err := git.ResolveRef(fmt.Sprintf(":%d:%s", stage, file))
105+
if err != nil {
106+
Exit("Could not checkout (are you not in the middle of a merge?): %v", err)
107+
}
108+
109+
scanner, err := git.NewObjectScanner()
110+
if err != nil {
111+
Exit("Could not create object scanner: %v", err)
112+
}
113+
114+
if !scanner.Scan(ref.Sha) {
115+
Exit("Could not find object %q", ref.Sha)
116+
}
117+
118+
ptr, err := lfs.DecodePointer(scanner.Contents())
119+
if err != nil {
120+
Exit("Could not find decoder pointer for object %q: %v", ref.Sha, err)
121+
}
122+
123+
p := &lfs.WrappedPointer{Name: file, Pointer: ptr}
124+
125+
if err := singleCheckout.RunToPath(p, checkoutTo); err != nil {
126+
Exit("Error checking out %v to %q: %v", ref.Sha, checkoutTo, err)
127+
}
128+
singleCheckout.Close()
129+
}
130+
131+
func whichCheckout() (stage git.IndexStage, err error) {
132+
seen := 0
133+
stage = git.IndexStageDefault
134+
135+
if checkoutBase {
136+
seen++
137+
stage = git.IndexStageBase
138+
}
139+
if checkoutOurs {
140+
seen++
141+
stage = git.IndexStageOurs
142+
}
143+
if checkoutTheirs {
144+
seen++
145+
stage = git.IndexStageTheirs
146+
}
147+
148+
if seen > 1 {
149+
return 0, fmt.Errorf("At most one of --base, --theirs, and --ours is allowed")
150+
}
151+
return stage, nil
152+
}
153+
78154
// Parameters are filters
79155
// firstly convert any pathspecs to the root of the repo, in case this is being
80156
// executed in a sub-folder
@@ -92,5 +168,10 @@ func rootedPaths(args []string) []string {
92168
}
93169

94170
func init() {
95-
RegisterCommand("checkout", checkoutCommand, nil)
171+
RegisterCommand("checkout", checkoutCommand, func(cmd *cobra.Command) {
172+
cmd.Flags().StringVar(&checkoutTo, "to", "", "Checkout a conflicted file to this path")
173+
cmd.Flags().BoolVar(&checkoutOurs, "ours", false, "Checkout our version of a conflicted file")
174+
cmd.Flags().BoolVar(&checkoutTheirs, "theirs", false, "Checkout their version of a conflicted file")
175+
cmd.Flags().BoolVar(&checkoutBase, "base", false, "Checkout the base version of a conflicted file")
176+
})
96177
}

commands/pull.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type abstractCheckout interface {
4343
Manifest() *tq.Manifest
4444
Skip() bool
4545
Run(*lfs.WrappedPointer)
46+
RunToPath(*lfs.WrappedPointer, string) error
4647
Close()
4748
}
4849

@@ -81,9 +82,7 @@ func (c *singleCheckout) Run(p *lfs.WrappedPointer) {
8182
return
8283
}
8384

84-
gitfilter := lfs.NewGitFilter(cfg)
85-
err = gitfilter.SmudgeToFile(cwdfilepath, p.Pointer, false, c.manifest, nil)
86-
if err != nil {
85+
if err := c.RunToPath(p, cwdfilepath); err != nil {
8786
if errors.IsDownloadDeclinedError(err) {
8887
// acceptable error, data not local (fetch not run or include/exclude)
8988
LoggedError(err, "Skipped checkout for %q, content not local. Use fetch to download.", p.Name)
@@ -99,6 +98,13 @@ func (c *singleCheckout) Run(p *lfs.WrappedPointer) {
9998
}
10099
}
101100

101+
// RunToPath checks out the pointer specified by p to the given path. It does
102+
// not perform any sort of sanity checking or add the path to the index.
103+
func (c *singleCheckout) RunToPath(p *lfs.WrappedPointer, path string) error {
104+
gitfilter := lfs.NewGitFilter(cfg)
105+
return gitfilter.SmudgeToFile(path, p.Pointer, false, c.manifest, nil)
106+
}
107+
102108
func (c *singleCheckout) Close() {
103109
if err := c.gitIndexer.Close(); err != nil {
104110
LoggedError(err, "Error updating the git index:\n%s", c.gitIndexer.Output())
@@ -117,6 +123,10 @@ func (c *noOpCheckout) Skip() bool {
117123
return true
118124
}
119125

126+
func (c *noOpCheckout) RunToPath(p *lfs.WrappedPointer, path string) error {
127+
return nil
128+
}
129+
120130
func (c *noOpCheckout) Run(p *lfs.WrappedPointer) {}
121131
func (c *noOpCheckout) Close() {}
122132

t/t-checkout.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,43 @@ begin_test "checkout: write-only file"
182182
popd > /dev/null
183183
)
184184
end_test
185+
186+
begin_test "checkout: conflicts"
187+
(
188+
set -e
189+
190+
reponame="checkout-conflicts"
191+
filename="file1.dat"
192+
193+
setup_remote_repo_with_file "$reponame" "$filename"
194+
195+
pushd "$TRASHDIR" > /dev/null
196+
clone_repo "$reponame" "${reponame}_checkout"
197+
198+
git tag base
199+
git checkout -b first
200+
echo "abc123" > file1.dat
201+
git add -u
202+
git commit -m "first"
203+
204+
git lfs checkout --to base.txt --base file1.dat 2>&1 | tee output.txt
205+
grep 'Could not checkout.*not in the middle of a merge' output.txt
206+
207+
git checkout -b second master
208+
echo "def456" > file1.dat
209+
git add -u
210+
git commit -m "second"
211+
212+
# This will cause a conflict.
213+
! git merge first
214+
215+
git lfs checkout --to base.txt --base file1.dat
216+
git lfs checkout --to ours.txt --ours file1.dat
217+
git lfs checkout --to theirs.txt --theirs file1.dat
218+
219+
echo "file1.dat" | cmp - base.txt
220+
echo "abc123" | cmp - theirs.txt
221+
echo "def456" | cmp - ours.txt
222+
popd > /dev/null
223+
)
224+
end_test

0 commit comments

Comments
 (0)