Skip to content

Commit b8f9b11

Browse files
authored
Merge pull request git-lfs#3296 from bk2204/checkout-conflict
Checkout options for conflicts
2 parents 48a931c + 35ba22d commit b8f9b11

File tree

5 files changed

+168
-7
lines changed

5 files changed

+168
-7
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

docs/man/git-lfs-checkout.1.ronn

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ git-lfs-checkout(1) -- Update working copy with file content if available
44
## SYNOPSIS
55

66
`git lfs checkout` <filespec>...
7+
`git lfs checkout` --to <path> { --ours | --theirs | --base } <file>...
78

89
## DESCRIPTION
910

10-
This command is deprecated, and should be replaced with `git checkout`.
11-
1211
Try to ensure that the working copy contains file content for Git LFS objects
1312
for the current ref, if the object data is available. Does not download any
14-
content, see git-lfs-fetch(1) for that.
13+
content, see git-lfs-fetch(1) for that.
1514

1615
Checkout scans the current ref for all LFS objects that would be required, then
1716
where a file is either missing in the working copy, or contains placeholder
@@ -20,6 +19,28 @@ we have it in the local store. Modified files are never overwritten.
2019

2120
Filespecs can be provided as arguments to restrict the files which are updated.
2221

22+
When used with `--to` and the working tree is in a conflicted state due to a
23+
merge, this option checks out one of the three stages of the conflict into a
24+
separate file. This can make using diff tools to inspect and resolve merges
25+
easier.
26+
27+
## OPTIONS
28+
29+
* `--base`:
30+
Check out the merge base of the specified file.
31+
32+
* `--ours`:
33+
Check out our side (that of the current branch) of the conflict for the
34+
specified file.
35+
36+
* `--theirs`:
37+
Check out their side (that of the other branch) of the conflict for the
38+
specified file.
39+
40+
* `--to` <path>:
41+
If the working tree is in a conflicted state, check out the portion of the
42+
conflict specified by `--base`, `--ours`, or `--theirs` to the given path.
43+
2344
## EXAMPLES
2445

2546
* Checkout all files that are missing or placeholders

git/git.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ const (
3838
RefBeforeFirstCommit = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
3939
)
4040

41+
type IndexStage int
42+
43+
const (
44+
IndexStageDefault IndexStage = iota
45+
IndexStageBase
46+
IndexStageOurs
47+
IndexStageTheirs
48+
)
49+
4150
// Prefix returns the given RefType's prefix, "refs/heads", "ref/remotes",
4251
// etc. It returns an additional value of either true/false, whether or not this
4352
// given ref type has a prefix.

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)