1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
//! Password store synchronization functionality.

use std::path::Path;
use std::time::Duration;

use anyhow::Result;

use crate::{
    git::{self, RepositoryState},
    Store,
};

/// Store git directory.
pub const STORE_GIT_DIR: &str = ".git/";

/// Duration after which pull refs are considered outdated.
///
/// If the last pull is within this duration, some operations such as a push may be optimized away
/// if not needed.
pub const GIT_PULL_OUTDATED: Duration = Duration::from_secs(30);

/// Sync helper for given store.
pub struct Sync<'a> {
    /// The store.
    store: &'a Store,
}

impl<'a> Sync<'a> {
    /// Construct new sync helper for given store.
    pub fn new(store: &'a Store) -> Sync<'a> {
        Self { store }
    }

    /// Get the repository path.
    fn path(&self) -> &Path {
        &self.store.root
    }

    /// Check readyness of store for syncing.
    ///
    /// This checks whether the repository state is clean, which means that there's no active
    /// merge/rebase/etc.
    /// The repository might be dirty, use `sync_is_dirty` to check that.
    pub fn readyness(&self) -> Result<Readyness> {
        let path = self.path();

        if !self.is_init() {
            return Ok(Readyness::NoSync);
        }

        match git::git_state(path).unwrap() {
            RepositoryState::Clean => {
                if is_dirty(path)? {
                    Ok(Readyness::Dirty)
                } else {
                    Ok(Readyness::Ready)
                }
            }
            state => Ok(Readyness::RepoState(state)),
        }
    }

    /// Prepare the store for new changes.
    ///
    /// - If sync is not initialized, it does nothing.
    /// - If sync remote is set, it pulls changes.
    pub fn prepare(&self) -> Result<()> {
        // TODO: return error if dirty?

        // Skip if no sync
        if !self.is_init() {
            return Ok(());
        }

        // We're done if we don't have a remote
        if !self.has_remote()? {
            return Ok(());
        }

        // We must have upstream set, otherwise try to automatically set or don't pull
        let repo = self.path();
        if git::git_branch_upstream(repo, "HEAD")?.is_none() {
            // Get remotes, we cannot decide upstream if we don't have exactly one
            let remotes = git::git_remote(repo)?;
            if remotes.len() != 1 {
                return Ok(());
            }

            // Fetch remote branches
            let remote = &remotes[0];
            git::git_fetch(repo, Some(remote))?;

            // List remote branches, stop if there are none
            let remote_branches = git::git_branch_remote(repo)?;
            if remote_branches.is_empty() {
                return Ok(());
            }

            // Determine upstream reference
            let branch = git::git_current_branch(repo)?;
            let upstream_ref = format!("{}/{}", remote, branch);

            // Set upstream reference if available on remote, otherwise stop
            if !remote_branches.contains(&upstream_ref) {
                return Ok(());
            }
            git::git_branch_set_upstream(repo, None, &upstream_ref)?;
        }

        self.pull()?;

        Ok(())
    }

    /// Finalize the store with new changes.
    ///
    /// - If sync is not initialized, it does nothing.
    /// - If sync is initialized, it commits changes.
    /// - If sync remote is set, it pushes changes.
    pub fn finalize<M: AsRef<str>>(&self, msg: M) -> Result<()> {
        // Skip if no sync
        if !self.is_init() {
            return Ok(());
        }

        // Commit changes if dirty
        if is_dirty(self.path())? {
            self.commit_all(msg, false)?;
        }

        // Do not push  if no remote or not out of sync
        if !self.has_remote()? || !safe_need_to_push(self.path()) {
            return Ok(());
        }

        // We must have upstream set, otherwise try to automatically set or don't push
        let mut set_branch = None;
        let mut set_upstream = None;
        let repo = self.path();
        if git::git_branch_upstream(repo, "HEAD")?.is_none() {
            // Get remotes, we cannot decide upstream if we don't have exactly one
            let remotes = git::git_remote(repo)?;
            if remotes.len() == 1 {
                // Fetch and list remote branches
                let remote = &remotes[0];
                git::git_fetch(repo, Some(remote))?;
                let remote_branches = git::git_branch_remote(repo)?;

                // Determine upstream reference
                let branch = git::git_current_branch(repo)?;
                let upstream_ref = format!("{}/{}", remote, branch);

                // Set upstream reference if not yet used on remote
                if !remote_branches.contains(&upstream_ref) {
                    set_branch = Some(branch);
                    set_upstream = Some(remote.to_string());
                }
            }
        }

        self.push(set_branch.as_deref(), set_upstream.as_deref())?;

        Ok(())
    }

    /// Initialize sync.
    pub fn init(&self) -> Result<()> {
        git::git_init(self.path())?;
        self.commit_all("Initialize sync with git", true)?;
        Ok(())
    }

    /// Clone sync from a remote URL.
    pub fn clone(&self, url: &str, quiet: bool) -> Result<()> {
        let path = self
            .path()
            .to_str()
            .expect("failed to determine clone path");
        git::git_clone(self.path(), url, path, quiet)?;
        Ok(())
    }

    /// Check whether sync has been initialized in this store.
    pub fn is_init(&self) -> bool {
        self.path().join(STORE_GIT_DIR).is_dir()
    }

    /// Get a list of sync remotes.
    pub fn remotes(&self) -> Result<Vec<String>> {
        git::git_remote(self.path())
    }

    /// Get the URL of the given remote.
    pub fn remote_url(&self, remote: &str) -> Result<String> {
        git::git_remote_get_url(self.path(), remote)
    }

    /// Add the URL of the given remote.
    pub fn add_remote_url(&self, remote: &str, url: &str) -> Result<()> {
        git::git_remote_add(self.path(), remote, url)
    }

    /// Set the URL of the given remote.
    pub fn set_remote_url(&self, remote: &str, url: &str) -> Result<()> {
        // Do not set but remove and add to flush any fetched remote data
        git::git_remote_remove(self.path(), remote)?;
        self.add_remote_url(remote, url)
    }

    /// Check whether this store has a remote configured.
    pub fn has_remote(&self) -> Result<bool> {
        if !self.is_init() {
            return Ok(false);
        }
        git::git_has_remote(self.path())
    }

    /// Pull changes from remote.
    fn pull(&self) -> Result<()> {
        git::git_pull(self.path())
    }

    /// Push changes to remote.
    fn push(&self, set_branch: Option<&str>, set_upstream: Option<&str>) -> Result<()> {
        git::git_push(self.path(), set_branch, set_upstream)
    }

    /// Add all changes and commit them.
    fn commit_all<M: AsRef<str>>(&self, msg: M, commit_empty: bool) -> Result<()> {
        let path = self.path();
        git::git_add_all(path)?;
        git::git_commit(path, msg.as_ref(), commit_empty)
    }
}

/// Defines readyness of store sync.
///
/// Some states block sync usage, including:
/// - Sync not initialized
/// - Git repository is dirty
#[derive(Debug)]
pub enum Readyness {
    /// Sync is not initialized for this store.
    NoSync,

    /// Special repository state.
    RepoState(git::RepositoryState),

    /// Repository is dirty (has uncommitted changes).
    Dirty,

    /// Ready to sync.
    Ready,
}

impl Readyness {
    /// Check if ready.
    pub fn is_ready(&self) -> bool {
        match self {
            Self::Ready => true,
            _ => false,
        }
    }
}

/// Check if repository is dirty.
///
/// Repository is dirty if it has any uncommitted changed.
fn is_dirty(repo: &Path) -> Result<bool> {
    git::git_has_changes(repo)
}

/// Check whether we need to push to the remote.
///
/// This defaults to true on error.
fn safe_need_to_push(repo: &Path) -> bool {
    match need_to_push(repo) {
        Ok(push) => push,
        Err(err) => {
            eprintln!(
                "failed to test if local branch is different than remote, ignoring: {}",
                err,
            );
            true
        }
    }
}

/// Check whether we need to push to the remote.
///
/// If the upstream branch is unknown, this always returns true.
fn need_to_push(repo: &Path) -> Result<bool> {
    // If last pull is outdated, always push
    let last_pulled = git::git_last_pull_time(repo)?;
    if last_pulled.elapsed()? > GIT_PULL_OUTDATED {
        return Ok(true);
    }

    // Get branch and upstream branch name
    let branch = git::git_current_branch(repo)?;
    let upstream = match git::git_branch_upstream(repo, &branch)? {
        Some(upstream) => upstream,
        None => return Ok(true),
    };

    // Compare local and remote branch hashes
    Ok(git::git_ref_hash(repo, branch)? != git::git_ref_hash(repo, upstream)?)
}