github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/checkoutindex.go (about)

     1  package git
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strconv"
    11  	"strings"
    12  )
    13  
    14  // CheckoutIndexOptions represents the options that may be passed to
    15  // "git checkout-index"
    16  type CheckoutIndexOptions struct {
    17  	UpdateStat bool
    18  
    19  	Quiet bool
    20  	Force bool
    21  	All   bool
    22  
    23  	NoCreate bool
    24  
    25  	Prefix string
    26  
    27  	// Stage not implemented
    28  	Stage string // <number>|all
    29  
    30  	Temp bool
    31  
    32  	// Stdin implies checkout-index with the --stdin parameter.
    33  	// nil implies it wasn't passed.
    34  	Stdin         io.Reader // nil implies no --stdin param passed
    35  	NullTerminate bool
    36  }
    37  
    38  // Performs CheckoutIndex on the Stdin io.Reader from opts, with the git index
    39  // passed as a parameter.
    40  func CheckoutIndexFromReaderUncommited(c *Client, idx *Index, opts CheckoutIndexOptions) error {
    41  	if opts.Stdin == nil {
    42  		return fmt.Errorf("Invalid Reader for opts.Stdin")
    43  	}
    44  	reader := bufio.NewReader(opts.Stdin)
    45  
    46  	var delim byte = '\n'
    47  	if opts.NullTerminate {
    48  		delim = 0
    49  	}
    50  
    51  	var f File
    52  	for s, err := reader.ReadString(delim); err == nil; s, err = reader.ReadString(delim) {
    53  		f = File(strings.TrimSuffix(s, string(delim)))
    54  
    55  		e := CheckoutIndexUncommited(c, idx, opts, []File{f})
    56  		if e != nil {
    57  			fmt.Fprintln(os.Stderr, e)
    58  		}
    59  	}
    60  	return nil
    61  }
    62  
    63  // Performs a CheckoutIndex on the files read from opts.Stdin
    64  func CheckoutIndexFromReader(c *Client, opts CheckoutIndexOptions) error {
    65  	idx, err := c.GitDir.ReadIndex()
    66  	if err != nil {
    67  		return err
    68  	}
    69  	return CheckoutIndexFromReaderUncommited(c, idx, opts)
    70  }
    71  
    72  // Handles checking out a file when --temp is specified on the command line.
    73  func checkoutTemp(c *Client, entry *IndexEntry, opts CheckoutIndexOptions) (string, error) {
    74  	// I don't know where ".merged_file" comes from
    75  	// for checkout-index, but it's what the real
    76  	// git client seems to use for a prefix..
    77  	tmpfile, err := ioutil.TempFile(".", ".merge_file_")
    78  	if err != nil {
    79  		return "", err
    80  	}
    81  	defer tmpfile.Close()
    82  
    83  	obj, err := c.GetObject(entry.Sha1)
    84  	if err != nil {
    85  		return "", err
    86  	}
    87  	_, err = tmpfile.Write(obj.GetContent())
    88  	if err != nil {
    89  		return "", err
    90  	}
    91  
    92  	os.Chmod(tmpfile.Name(), os.FileMode(entry.Mode))
    93  	return tmpfile.Name(), nil
    94  }
    95  
    96  // Checks out a given index entry.
    97  func checkoutFile(c *Client, entry *IndexEntry, opts CheckoutIndexOptions) error {
    98  	f, err := entry.PathName.FilePath(c)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	f = File(opts.Prefix) + f
   103  	if f.Exists() && !opts.Force {
   104  		if !opts.Quiet {
   105  			return fmt.Errorf("%v already exists, no checkout", entry.PathName.String())
   106  		}
   107  		return nil
   108  	}
   109  
   110  	obj, err := c.GetObject(entry.Sha1)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	if !opts.NoCreate {
   115  		fmode := os.FileMode(entry.Mode)
   116  		if f.Exists() && f.IsDir() {
   117  			if err := os.RemoveAll(f.String()); err != nil {
   118  				return err
   119  			}
   120  		}
   121  
   122  		p := filepath.Dir(f.String())
   123  		if f := File(p); !f.Exists() {
   124  			if err := os.MkdirAll(p, 0777); err != nil {
   125  				return err
   126  			}
   127  		} else if !f.IsDir() {
   128  			// FIXME: This shouldn't be required, this
   129  			// should be handled by being returned by
   130  			// ls-files -k before we get to this point.
   131  			if err := os.Remove(f.String()); err != nil {
   132  				return err
   133  			}
   134  			if err := os.MkdirAll(p, 0777); err != nil {
   135  				return err
   136  			}
   137  		}
   138  		err := ioutil.WriteFile(f.String(), obj.GetContent(), fmode)
   139  		if err != nil {
   140  			return err
   141  		}
   142  		os.Chmod(f.String(), os.FileMode(entry.Mode))
   143  	}
   144  
   145  	// Update the stat information, but only if it's the same
   146  	// file name. We only change the mtime, and ctime because the only
   147  	// other thing we track is the file size, and that couldn't
   148  	// have changed.
   149  	// Don't change the stat info if there's a prefix, because
   150  	// if we checkout out into a prefix, it means we haven't
   151  	// touched the index.
   152  	if opts.Prefix == "" && opts.UpdateStat {
   153  		if err := entry.RefreshStat(c); err != nil {
   154  			return err
   155  		}
   156  	}
   157  	return nil
   158  }
   159  
   160  // Same as "git checkout-index", except the Index is passed as a parameter (and
   161  // may not have been written to disk yet). You likely want CheckoutIndex instead.
   162  //
   163  // (This is primarily for read-tree to be able to update the filesystem with the
   164  // -u parameter.)
   165  func CheckoutIndexUncommited(c *Client, idx *Index, opts CheckoutIndexOptions, files []File) error {
   166  	if opts.All {
   167  		files = make([]File, 0, len(idx.Objects))
   168  		for _, entry := range idx.Objects {
   169  			f, err := entry.PathName.FilePath(c)
   170  			if err != nil {
   171  				return err
   172  			}
   173  			files = append(files, f)
   174  		}
   175  	}
   176  
   177  	killed, err := LsFiles(c, LsFilesOptions{Killed: true}, files)
   178  	if err != nil {
   179  		return err
   180  	}
   181  	if len(killed) > 0 {
   182  		if !opts.Force {
   183  			msg := ""
   184  			for i, path := range killed {
   185  				if i > 0 {
   186  					msg += "\n"
   187  
   188  				}
   189  				f, err := path.PathName.FilePath(c)
   190  				if err != nil {
   191  					return err
   192  				}
   193  				msg += fmt.Sprintf("fatal: cannot create directory at '%v': File exists", f)
   194  			}
   195  			return fmt.Errorf("%v", msg)
   196  		} else {
   197  			for _, file := range killed {
   198  				f, err := file.PathName.FilePath(c)
   199  				if err != nil {
   200  					return err
   201  				}
   202  
   203  				if err := os.RemoveAll(f.String()); err != nil {
   204  					return err
   205  				}
   206  			}
   207  		}
   208  	}
   209  
   210  	var stageMap map[IndexStageEntry]*IndexEntry
   211  	if opts.Stage == "all" {
   212  		// This is only used if stage==all, but we don't want to reallocate
   213  		// it every iteration of the loop, so we just define it as a var
   214  		// and let it stay nil unless stage=="all"
   215  		stageMap = idx.GetStageMap()
   216  	}
   217  	var delim byte = '\n'
   218  	if opts.NullTerminate {
   219  		delim = 0
   220  	}
   221  
   222  	for _, file := range files {
   223  		fname := File(file)
   224  		indexpath, err := fname.IndexPath(c)
   225  		if err != nil {
   226  			if !opts.Quiet {
   227  				fmt.Fprintf(os.Stderr, "%v\n", err)
   228  			}
   229  			continue
   230  		}
   231  
   232  		if opts.Stage == "all" {
   233  			if _, ok := stageMap[IndexStageEntry{indexpath, Stage0}]; ok {
   234  				if !opts.Quiet {
   235  					// This error doesn't make any sense to me,
   236  					// but it's what the official git client says when
   237  					// you try and checkout-index --stage=all a file that isn't
   238  					// in conflict.
   239  					fmt.Fprintf(os.Stderr, "git checkout-index: %v does not exist at stage 0\n", indexpath)
   240  				}
   241  				continue
   242  			}
   243  			if stg1, s1ok := stageMap[IndexStageEntry{indexpath, Stage1}]; s1ok {
   244  				name, err := checkoutTemp(c, stg1, opts)
   245  				if err != nil {
   246  					fmt.Fprintln(os.Stderr, err)
   247  				}
   248  				fmt.Print(name, " ")
   249  			} else {
   250  				fmt.Print(". ")
   251  			}
   252  			if stg2, s2ok := stageMap[IndexStageEntry{indexpath, Stage2}]; s2ok {
   253  				name, err := checkoutTemp(c, stg2, opts)
   254  				if err != nil {
   255  					fmt.Fprintln(os.Stderr, err)
   256  				}
   257  				fmt.Print(name, " ")
   258  			} else {
   259  				fmt.Print(". ")
   260  			}
   261  			if stg3, s3ok := stageMap[IndexStageEntry{indexpath, Stage3}]; s3ok {
   262  				name, err := checkoutTemp(c, stg3, opts)
   263  				if err != nil {
   264  					fmt.Fprintln(os.Stderr, err)
   265  				}
   266  				fmt.Printf("%s\t%s%c", name, stg3.PathName, delim)
   267  			} else {
   268  				fmt.Printf(".\t%s%c", stg3.PathName, delim)
   269  			}
   270  			continue
   271  		}
   272  
   273  		for _, entry := range idx.Objects {
   274  			if entry.PathName != indexpath {
   275  				continue
   276  			}
   277  
   278  			if !opts.Temp && !opts.Force && opts.Prefix == "" && entry.PathName.IsClean(c, entry.Sha1) {
   279  				// don't bother checkout out the file
   280  				// if it's already clean. This makes us less
   281  				// likely to avoid GetObject have an error
   282  				// trying to read from a packfile (which isn't
   283  				// supported yet.)
   284  				// We don't check this if there's a prefix, since it's not checking out
   285  				// into the same location as the index.
   286  				// FIXME: This should use stat information, not hash
   287  				// the whole file.
   288  				continue
   289  			}
   290  
   291  			switch opts.Stage {
   292  			case "":
   293  				if entry.Stage() == Stage0 {
   294  					var name string
   295  					if opts.Temp {
   296  						name, err = checkoutTemp(c, entry, opts)
   297  						if name != "" {
   298  							fmt.Printf("%v\t%v%c", name, entry.PathName, delim)
   299  						}
   300  					} else {
   301  						err = checkoutFile(c, entry, opts)
   302  					}
   303  				} else {
   304  					return fmt.Errorf("Index has unmerged entries. Aborting.")
   305  				}
   306  			case "1", "2", "3":
   307  				stg, _ := strconv.Atoi(opts.Stage)
   308  				if entry.Stage() == Stage(stg) {
   309  					var name string
   310  
   311  					if opts.Temp {
   312  						name, err = checkoutTemp(c, entry, opts)
   313  						if name != "" {
   314  							fmt.Printf("%v\t%v%c", name, entry.PathName, delim)
   315  						}
   316  
   317  					} else {
   318  						err = checkoutFile(c, entry, opts)
   319  					}
   320  				}
   321  			default:
   322  				return fmt.Errorf("Invalid stage: %v", opts.Stage)
   323  			}
   324  			if err != nil {
   325  				fmt.Fprintln(os.Stderr, err)
   326  			}
   327  
   328  		}
   329  	}
   330  
   331  	if opts.UpdateStat {
   332  		f, err := c.GitDir.Create(File("index"))
   333  		if err != nil {
   334  			return err
   335  		}
   336  		defer f.Close()
   337  		return idx.WriteIndex(f)
   338  	}
   339  	return nil
   340  }
   341  
   342  // CheckoutIndex implements the "git checkout-index" subcommand of git.
   343  func CheckoutIndex(c *Client, opts CheckoutIndexOptions, files []File) error {
   344  	if len(files) != 0 && opts.All {
   345  		return fmt.Errorf("Can not mix --all and named files")
   346  	}
   347  
   348  	idx, err := c.GitDir.ReadIndex()
   349  	if err != nil {
   350  		return err
   351  	}
   352  	if opts.Stdin == nil {
   353  		return CheckoutIndexUncommited(c, idx, opts, files)
   354  	} else {
   355  		if len(files) != 0 {
   356  			return fmt.Errorf("Can not mix --stdin and paths on command line")
   357  		}
   358  		return CheckoutIndexFromReaderUncommited(c, idx, opts)
   359  	}
   360  }