Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

checkout removes staged files. #1741

Open
hannesdelbeke opened this issue Mar 7, 2023 · 9 comments
Open

checkout removes staged files. #1741

hannesdelbeke opened this issue Mar 7, 2023 · 9 comments

Comments

@hannesdelbeke
Copy link

hannesdelbeke commented Mar 7, 2023

Please be sure to mention:

  • whether you are using Node or the Browser
  • how you are using it (if you are using a bundler what bundler; if you are using a <script> tag what CDN URL)

This plugin is used in the obsidian android git plugin. There was a bug that deleted files in certain occasions. The plugin author mentioned this is a bug with isomorphic-git. Because checkout removes staged files.
For repro, and discussion see denolehov/obsidian-git#396 (comment)

@araknast
Copy link
Contributor

araknast commented Mar 21, 2023

Seems that the relevant lines are here where analyze() chooses to delete entries when they exist in the stage and workdir but not the commit. This can be changed but I believe the tests would need to be updated to match.

This behavior seems pretty intentional so I'll need to look into it a bit more before making changes.

@tmpmachine
Copy link
Contributor

For now we can read the blob from Index as shown here, then replace the content on working directory:
#1165 (comment)

I've edited the snippet to return a blob.

const readContentsFromHash = async (hash, gitDir, filePath = null) => {
  let config = {
    fs,
    dir: gitDir,
    oid: hash,
  };

  if (filePath) {
    config = {
      ...config,
      filepath: filePath,
    };
  }

  const { blob } = await git.readBlob(config);

  // return new TextDecoder().decode(blob);
  return blob;
};

const getStagedFileContents = async (gitDir, stagedFilePaths) => {
  const map = async (filePath, [A]) => {
    if (stagedFilePaths.includes(filePath)) {
      const contents = await readContentsFromHash(await A.oid(), gitDir);

      return {
        filePath: `/${filePath}`,
        contents,
      };
    }
  };

  return await git.walk({
    fs,
    dir: gitDir,
    trees: [git.STAGE()],
    map,
  });
};

// get content from staging
let items = await getStagedFileContents('/', ['style.css'])
// returns [ {filePath: '/style.css', contents: Uint8Array(4)} ]

// update the working directory
let newContent = items[0].contents;
await fs.promises.writeFile('/style.css', newContent);

@jcubic
Copy link
Contributor

jcubic commented Mar 21, 2024

I think that I've missed this issue. Can you show an example of canonical git sequence of commands and how it differs from isomorphic-git? It will help to understand if this is a bug or expected behavior.

@tmpmachine
Copy link
Contributor

tmpmachine commented Mar 21, 2024

Suppose I have style.css in HEAD:

  1. Make some modifications on style.css.
  2. git add style.css.
  3. Make some other modifications on style.css.
  4. git checkout -- style.css

In canonical git, style.css content is expected to be restored to INDEX (step 2), but isomorphic-git's checkout discard the whole changes (INDEX, WORKDIR) back to HEAD.

@jcubic
Copy link
Contributor

jcubic commented Mar 21, 2024

This is my old code that I used to checkout a single file:

async function readBranchFile({ dir, filepath, branch }) {
    const ref = 'refs/remotes/origin/' + branch;
    const sha = await git.resolveRef({ dir,  ref });
    const { object: { tree } } = await git.readObject({ dir, oid: sha });
    return (async function loop(tree, path) {
        if (!path.length) {
            throw new Error(`File ${filepath} not found`);
        }
        var name = path.shift();
        const { object: { entries } } = await git.readObject({ dir, oid: tree });
        const packageEntry = entries.find((entry) => {
            return entry.path === name;
        });
        if (!packageEntry) {
            throw new Error(`File ${filepath} not found`);
        } else {
            if (packageEntry.type == 'blob') {
                const { object: pkg } = await git.readObject({ dir, oid: packageEntry.oid });
                return pkg.toString('utf8');
            } else if (packageEntry.type == 'tree') {
                return loop(packageEntry.oid, path);
            }
        }
    })(tree, filepath.split('/'));
}

function gitCheckoutFile({dir, filepath, branch}) {
    var fname = dir + '/' + filepath;
    return new Promise(function(resolve, reject) {
        readBranchFile({dir, branch, filepath}).then(oldFile => {
            if (!oldFile) {
                return;
            }
            fs.writeFile(fname, oldFile, err => {
                if (err) {
                    reject(err);
                } else {
                    resolve();
                }
            });
        });
    });
}

NOTE that the code was written before version 1.0 came out.

@jcubic
Copy link
Contributor

jcubic commented Mar 21, 2024

I didn't test the current API, but it looks like the option is filepaths not filepath, and it's an array of strings. See https://isomorphic-git.org/docs/en/checkout

Can you share how you use the API?

@tmpmachine
Copy link
Contributor

tmpmachine commented Mar 22, 2024

@jcubic I adjust your code to v1.x and can confirm that git.checkout remove the changes in both INDEX & WORKDIR.

Adjustment:

  • Added fs option to git.
  • ref is branch name without full path, e.g. main or dev (for local repo).
  • Entry return type is blob (my app put blob directly into the file instead of a string).
async function readBranchFile({ dir, filepath, branch }) {
    // const ref = 'refs/remotes/origin/' + branch;
    const ref = branch;
    const sha = await git.resolveRef({ fs, dir,  ref });
    const { object: { tree } } = await git.readObject({ fs, dir, oid: sha });
    return (async function loop(tree, path) {
        if (!path.length) {
            throw new Error(`File ${filepath} not found`);
        }
        var name = path.shift();
        const result = await git.readObject({ fs, dir, oid: tree });
        // console.log(result)
        // const { object: { entries } } = result;
        const packageEntry = result.object.find((entry) => {
            return entry.path === name;
        });
        console.log(packageEntry)
        if (!packageEntry) {
            throw new Error(`File ${filepath} not found`);
        } else {
            if (packageEntry.type == 'blob') {
                const { object: pkg } = await git.readObject({ fs, dir, oid: packageEntry.oid });
                // return pkg.toString('utf8');
                return pkg;
            } else if (packageEntry.type == 'tree') {
                return loop(packageEntry.oid, path);
            }
        }
    })(tree, filepath.split('/'));
}

function gitCheckoutFile({dir, filepath, branch}) {
    var fname = dir + '/' + filepath;
    return new Promise(function(resolve, reject) {
        readBranchFile({dir, branch, filepath}).then(oldFile => {
            if (!oldFile) {
                return;
            }
            fs.writeFile(fname, oldFile, err => {
                if (err) {
                    reject(err);
                } else {
                    resolve();
                }
            });
        });
    });
}

What I, and possibly the folks on obsidian git plugin trying to do is to discard the changes on WORKDIR by running git checkout -- filename.txt command while keeping the INDEX unaffected. In below image, the expected content after checkout is "Hello", leaving only staged changes.

image

image

In isomorphic-git's checkout, however, the staged changes is seemingly unstaged and discarded along with changes in WORKDIR.

image

Here's the git command to test the behaviour in canonical git:

echo '' >> style.css
git add style.css
git commit -m "init"
echo 'Hello' >> style.css
git add style.css
echo ' World' >> style.css
 
git checkout -- style.css

And here's how I use it previously to discard changes in WORKDIR (which had some unexpected behaviour when having staged changes):

await git.checkout({
  fs,
  dir: '/',
  force: true,
  filepaths: ['style.css']
})

@jcubic
Copy link
Contributor

jcubic commented Mar 22, 2024

Ok, so this is definitely a bug. Do you want to contribute a full solution that will match the API? You will need to add unit test to that, to test your scenario. You can add unit test first to make sure it's failing without the fix.

@tmpmachine
Copy link
Contributor

Not anytime soon I'm afraid. Guess I'll see if I can run the project first on Codespace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants