99%的时间在使用的Git命令

Git是目前最流行的分布式版本控制系统,它是Linus献给软件行业的两件礼物之一,另外一件礼物是目前最大的服务器系统软件Linux。

Git出现之前,linux的源代码使用BitMover公司的BitKeeper进行版本控制。这是一个商业的版本控制系统,一开始授权Linux社区免费使用,后来由于某种原因,BitMover公司打算收回了Linux社区的免费使用权。这个时候,Linus花了两周时间自己用C写了一个分布式版本控制系统,并在一个月之内托管了linux系统的源码,这一年,是2005年。

最常用的4条命令

Git是一个非常强大的工具,各种命令组合上千种。不过,我们90%的时间估计都在用这4条命令。

$ git status # 查看工作区和缓冲区状态
$ git add --all # 将工作区修改暂存到缓冲区
$ git commit -m"<comment>" # 提交到仓库
$ git push origin master # 推送到远程分支

如果只是个人开发,几天便结束的小项目,使用Git仅为了方便回退到某个点,记录开发进度,这几条命令当然就够了。

为了合作与规范,这还远远不够

关联远程仓库

  • 如果还没有Git仓库,你需要
$ git init
  • 如果你想关联远程仓库
$ git remote add <name> <git-repo-url>
# 例如 git remote add origin https://github.com/xxxxxx
  • 如果你想关联多个远程仓库
$ git remote add <name> <another-git-repo-url>
# 例如 git remote add coding https://coding.net/xxxxxx

是远程仓库的名称,通常为 origin

  • 忘了关联了哪些仓库或者地址
$ blog-react git:(master) git remote -v
# origin https://github.com/gzdaijie/koa-react-server-render-blog.git (fetch)
# origin https://github.com/gzdaijie/koa-react-server-render-blog.git (push)
  • 如果远程有仓库,你需要clone到本地
$ git clone <git-repo-url>

关联的远程仓库将被命名为origin,这是默认的。

  • 如果你想把别人仓库的地址改为自己的
$ git remote set-url origin <your-git-url>

切换分支

  • 新建仓库后,默认生成了master分支

  • 如果你想新建分支并切换

$ git checkout -b <new-branch-name>
# 例如 git checkout -b dev
# 如果仅新建,不切换,则去掉参数 -b
  • 看看当前有哪些分支
$ git branch
# * dev
#   master

标*号的代表当前所在的分支

  • 看看当前本地&远程有哪些分支
$ git branch -a
# * dev
#   master
#   remotes/origin/master
  • 切换到现有的分支
$ git checkout master
  • 你想把dev分支合并到master分支
$ git merge <branch-name>
# 例如 git merge dev
  • 你想把本地master分支推送到远程去
$ git push origin master

你可以使用git push -u origin master将本地分支与远程分支关联,之后仅需要使用git push即可。

  • 远程分支被别人更新了,你需要更新代码
$ git pull origin <branch-name>

之前如果push时使用过-u,那么就可以省略为git pull

  • 本地有修改,能不能先git pull
$ git stash # 工作区修改暂存
$ git pull  # 更新分支
$ git stash pop # 暂存修改恢复到工作区

撤销操作

  • 恢复暂存区文件到工作区
$ git checkout <file-name>
  • 恢复暂存区的所有文件到工作区
$ git checkout .
  • 重置暂存区的某文件,与上一次commit保持一致,但工作区不变
$ git reset <file-name>
  • 重置暂存区与工作区,与上一次commit保持一致
$ git reset --hard <file-name>

如果是回退版本(commit),那么file,变成commit的hash码就好了。

  • 去掉某个commit
$ git revert <commit-hash>

实质是新建了一个与原来完全相反的commit,抵消了原来commit的效果

版本回退与前进

  • 查看历史版本
$ git log
  • 你可能觉得这样的log不好看,试试这个
$ git log --graph --decorate --abbrev-commit --all
  • 检出到任意版本
$ git checkout a5d88ea

hash码很长,通常6-7位就够了

  • 远程仓库的版本很新,但是你还是想用老版本覆盖
$ git push origin master --force
# 或者 git push -f origin master
  • 觉得commit太多了?多个commit合并为1个
$ git rebase -i HEAD~4

这个命令,将最近4个commit合并为1个,HEAD代表当前版本。将进入VIM界面,你可以修改提交信息。推送到远程分支的commit,不建议这样做,多人合作时,通常不建议修改历史。

  • 想回退到某一个版本
$ git reset --hard <hash>
# 例如 git reset --hard a3hd73r

–hard代表丢弃工作区的修改,让工作区与版本代码一模一样,与之对应,–soft参数代表保留工作区的修改。

  • 想回退到上一个版本,有没有简便方法?
$ git reset --hard HEAD^
  • 回退到上上个版本呢?
$ git reset --hard HEAD^^

HEAD^^可以换作具体版本hash值。

  • 回退错了,能不能前进呀
$ git reflog

这个命令保留了最近执行的操作及所处的版本,每条命令前的hash值,则是对应版本的hash值。使用上述的git checkout 或者 git reset命令 则可以检出或回退到对应版本。

  • 刚才commit信息写错了,可以修改吗
$ git commit --amend
  • 看看当前状态吧
$ git status

配置属于你的Git

  • 看看当前的配置
$ git config --list
  • 估计你需要配置你的名字
$ git config --global user.name "<name>"

–global为可选参数,该参数表示配置全局信息

  • 希望别人看到你的commit可以联系到你
$ git config --global user.email "<email address>"
  • 有些命令很长,能不能简化一下
$ git config --global alias.logg "log --graph --decorate --abbrev-commit --all"

之后就可以开心地使用 git logg了

最后

这篇文章是对平时工作中最常用到的命令的一个总结,无论是初学者,还是已经使用了Git几年的你,都能有所收获吧。

如果你是第一次接触Git,那么这篇文章并不适合你,你需要先去了解Git的四个状态、三个区等概念。

基于 git 和 CI/CD 的集中化配置管理服务

分享一种基于 git 和 CI/CD 的集中化配置管理服务。这种方案最大的好处就是,简单直接,可以快速先把配置管理的坑儿占好。

功能点

首先,我们先整理一下集中化配置管理的主要 feature:

  • 可以记录、审核配置的修改
  • 支持多种环境(生产、测试、开发、演示等等)
  • 修改配置之后,应用的配置能够及时得到更新

主要思路

我们的主要思路是:将配置服务直接写成一个独立的 webserver,webserver 对外提供 http 接口,配置直接写在 webserver 的代码当中,每次提交代码时通过 CI/CD 自动发布。

这样做的好处是:

  • 可以直接通过 git 来记录、审核配置数据的修改,每次有人要修改配置时,直接提 PR,leader review 通过之后合并到 master 分支
  • 代码合并到 master 分支之后,通过 CI/CD 自动发布到线上

可能大家会有下面的一些顾虑:

  • 不应该直接在代码当中硬编码 MySQL 的账号、密码之类的敏感数据,这样是不安全的
  • 简单通过 http 接口来读取配置,效率不高

针对第一个问题,我们算是使用了点 “反模式” 吧。代码肯定是要确保在私有代码库当中的,你需要授权才能够访问代码库,从这个角度来说,直接在代码里面写配置数据其实也不是大问题,特别是在产品研发初期,这个时候团队规模也不大。而且,像 gitlab、github 这类的服务,本身就有很好的权限管理机制,加上 git 本身就是版本管理工具,为什么不充分使用一下呢。

第二个问题呢,其实和第一个一样:初期,服务压力较小,配置数据不复杂,通过 http 接口来读取配置,性能其实没有大问题。

这种方案的意义就在于把这个配置管理的坑儿先占上,确保各个服务是通过统一的接口来读取配置的,日后可以慢慢优化。 实践发现,随着产品迭代,这种方案能够持续的时间还是挺长的,投入成本还很小。

主要功能设计和实现

我们自己的项目使用 Node.js 开发的,所以下面以 Node.js 为例,来说一下具体设计。

首先,说一下 webserver 的接口设计,接口要尽可能简化,我们只提供了一个接口:

GET /api/profiles/:profile HTTP/1.1

profile 参数表示你想要的环境,比如:

  • 你想要测试环境的配置,应该发送 GET /api/profiles/dev
  • 如果想要同事 Jack 的本地开发环境配置,你应该发送 GET /api/profiles/jack-local-dev

返回的数据自然应该是 json 数据,比如像下面这种:

{
    "revision": "5d41402abc4b2a76b9719d911017c592",
    "config": {
        "debug": true,
        "wechat": {
            "appId": "xxxx",
            "secret": "xxxxx"
        },
        "mysql": {
            "host": "localhost",
            "port": 3306
        }
    }
}

revision 表示配置的版本,config 就是实际的配置数据啦。

根据上面的设计,我们的 webserver 服务的代码库大概是下面这样的:

├── Dockerfile
├── README.md
├── app.js
└──  config
    ├── dev.yml
    ├── prod.yml
    └── jack-local-dev.yml

其中:

  • 有一个 app.js ,里面封装了 http 接口
  • 有一个 config 文件夹,里面放置不同环境的配置文件。我们推荐使用 .yml 文件,.yml 文件写起配置其实更清爽,当然 json 也可以
  • 再有一个 Dockerfile 用于配置镜像打包和自动发布

实现这样一个接口,app.js 的代码也比较简单,大概就像下面这样:

const fs = require('fs');
const yaml = require('js-yaml');
const hash = require('object-hash');
const express = require('express');

const app = express();
app.get('/api/profiles/:profile', (req, res) => {
    const path = `${__dirname}/config/${req.params.profile}.yml`
    fs.readFile(path, {
        "ecoding": "utf-8"
    }, (e, content) => {
        if (e) {
            return res.status(500).json({
                errorId: 'internal-server-error',
                errorMsg: e.message
            });
        }

        const config = yaml.safeLoad(content);
        const revision = hash(config);
        res.json({
            config,
            revision
        });
    });
});

app.get('/ping', (req, res) => res.send('pong'));

const PORT = 8080;
app.listen(PORT, () => {
  console.log('listening on port', PORT);
});

当然你也可以在上面加一些性能上的优化哈,特别是加载 yaml 文件的部分。除了读取 yaml 配置文件的内容外,里面还通过 object-hash 来计算了配置的 revision,方便客户端来检查配置数据的版本更新。

提供统一的客户端 library

主体设计和实现就是上面说的这些内容了。不过,还有一项工作很重要,就是提供统一的客户端 library。当大家使用同样的客户端 library 来读取配置的时候,配置管理的坑儿才能算真正占好,后面才方便替换配置管理服务的技术方案。

library 设计

首先说一下这个 library 的接口设计吧

config.get(path)

提供一个 get 方法,注意:

  • 参数里面应该是一个 path,准确的说应该是一个 property path
  • 这个方法应该是 同步执行 的,所以下面我提供了一个 sync 方法,专门用来同步配置数据

假设完整的配置数据是这样的:

{
    "mysql": {
        "host": "111.111.11.11",
        "port": 3306,
        "username": "root",
        "password": "123456"
    },
    "redis": {
        "host": "111.111.11.12",
        "port": 6379
    },
    "wechat": {
        "appId": "wx888888888"
    },
    "secret": "foobar"
}

那么通过 get 方法应该能够做到下面这些事情:

config.get("mysql") 
// => {"host": "111.111.11.11", "port": 3306, ...}

config.get("wechat.appId") 
// => "wx888888888"

config.get() 
// => {"mysql": {...}, "wechat": {...}, ...}

也就是说,大家可以通过 get 方法灵活的获取到配置数据的某一部分。这块我们使用了 object-path 这个模块。

config.sync(host, profile, token)

提供一个 sync 方法,用来初始话和轮训同步配置数据

config.on(event, listener)

应该提供事件回调接口,用来检测是否有数据发生变化,这个接口在 Node.js 服务中有一定用处,其他的同步的技术框架应该就不需要了。

config.mock(object)

最后,应该有一个 mock 方法,方便支持自动化测试

一些补充内容

这里想补充说明的是,关于 sync 方法的一些小问题。上面说到 get 方法应该是一个同步方法,毕竟如果读取配置信息也要异步的话,那对工程的来说复杂度反而增加了。

所以我多设计了一个 sync 方法。在 Node.js 项目中,应用启动之前,应该先调用 sync 方法,轮训同步配置数据。这样保证 get 方法被调用的时候,始终是能够返回数据的。

还有一点就是,sync 方法被调用的时候,应该先发一个 同步的 http 方法来获取数据,这块我们使用了 sync-request 来实现。

最后,补充一下主要的实现代码,供大家参考:

const EventEmitter = require('events').EventEmitter;
const objectPath = require('object-path');

class Config {
  constructor(interval) {
    this.interval = interval || 5000;
    this.emitter = new EventEmitter();
  }

  sync(host, profile, token) {
    this.host = host;
    this.profile = profile;
    this.token = token;

    this.data = loadConfigSync(); // 首先同步获取配置数据
    setTimeOut(() => this.watch(), this.interval); // 之后,定时轮训数据
  }

  get(path) {
      return objectPath.get(this.data.config, path);
  }

  loadConfigSync() {
      // 这部分代码就先省略了~
  }

  async loadConfigAsync() {
      // 这部分代码就先省略了~
  }

  async watch() {
    const result = await this.loadConfigAsync();
    if (result.revision !== this.data.revision) {
        this.data = result;
        this.emitter.emit('update', this.data.config);
    }

    setTimeout(() => this.watch(), this.interval);
  }
}

module.exports = new Config();

使用客户端 library 的一般套路:

// server.js
const config = require('config-module-name');

// 1. 调用 sync 方法加载配置
config.sync(process.env.CONFIG_HOST, process.env.CONFIG_PROFILE, process.env.CONFIG_TOKEN);

// 2. 启动实际项目的 WebServer
const server = new WebServer();
server.serve();

增加配置覆盖功能

上面的 webserver 设计还是简单了一些,因为平时我们配置服务的时候,经常会有一系列通用的配置,而每个环境里面可能各有一些少量特殊的配置。

为了解决这个问题,我们在前面的方案基础之上,开发了一个简单的配置覆盖功能。我们是这么做的:

  • 在 config 文件夹当中提供一个 defaul.yml 配置文件,在这个文件当中去保存通用的配置数据
  • 假设,现在要访问 dev 环境的配置,webserver 就把 dev.yml 和 default.yml 配置文件都读取出来,将 dev.yml 和 default.yml 重合的部分 merge 到一起,这块我们使用的一个叫做 deepmerge 的模块来实现的

现在举一个实际的例子,假设生产(prod)和开发环境(dev)就数据库的名称不同,没有增加配置覆盖功能之前,配置文件是这样的:

# prod.yml
mysql:
    host: localhost
    port: 3306
    username: root
    password: root
    database: prod

# dev.yml
mysql:
    host: localhost
    port: 3306
    username: root
    password: root
    database: de

增加了配置覆盖的功能之后,配置文件变成了下面这个样子:

# default.yml
mysql:
    host: localhost
    port: 3306
    usrename: root
    password: root

# prod.yml
mysql:
    database: prod

# dev.yml
mysql:
    database: dev

在实际的项目当中,增加配置覆盖的一个最大好处是,有新的同事加入项目时,他需要增加的配置内容就会少很多,而不需要全量的 copy 一份别人的配置文件,主体的配置都可以放到 default.yml 文件中。

安全问题

这个方案现在还有一些明显的安全问题:

  • 接口访问没有增加鉴权
  • 有些数据就是不希望写到代码当中去,该怎么办

关于接口鉴权,我们的解决方案是提供一个 token 列表,token 是常量的 UUID 或者随机字符串即可。另外强制要求使用 https 来访问接口,不要直接在前端读取配置。

如果有些数据就是不希望写到代码当中去,改怎么办?

我们建议增加一个环境变量注入的 feature,比如配置文件改写成这样:

mysql:
    password: ${MYSQL_PASSWORD}

接口在返回数据之前,增加一道工序,将上面的 ${MYSQL_PASSWORD} 这类的表达式解析出来,然后将环境变量注入进去。我们目前是使用正则表达式简单粗暴的处理的,大概就是这样:

const traverse = require('traverse');
const delimeter = /${(.+?)}/g;

function enjectEnv(config) {
    return traverse(config).map(value => {
        value.replace(delimeter, (match, p1) => {
            return process.env[p1] || "";
        })
    })
}

通过这种方式,你就可以通过环境变量去配置一些敏感信息了。

总结

总结一下,这样一个方案,主要的工作:

  • 基于 git 和 CI/CD 搭建配置服务
  • 提供统一的客户端 library
  • 扩展功能,增加配置覆盖机制
  • 提供简单的接口鉴权和环境变量注入

这样一个方案,其实在产品初期阶段应该足够好用了。这种方案的好处就是快速占坑,将配置管理机制固化下来。整套方案充分使用了 git 和 CI/CD,整个服务也很轻,推荐大家尝试一下~

重写git历史记录

大家在使用git时做版本管理时,有时候会遇到下面的场景,

  • 不小心将一个很大的文件提交到仓库中了,导致仓库臃肿,上传下载都非常消耗网络,追悔莫及!

  • 随着项目的推进,突然不想用git来管理某个文件了,将其放入.gitignore文件中,发现git依然能够探测到这个文件的改动,一脸懵逼!

本文将会就以上两个问题给你指条明路。

git filter-branch命令就是我们的主角。git filter-branch命令在git中号称大杀器命令,基本上可以用这个命令能够到达任何操作要求,注意,是任何!

关于这个命令的文档我已经在上面的链接中给出了,一句话概括这个命令的作用:操作所有的git对象数据以重写历史记录。有兴趣掌握其原理的,可自行找虐。下面,我们将会用一个实际的案例来说明“如何重写git历史记录”。

案例

在前端开发中,npm是必不可少的包管理工具。在[email protected]之后,npm引入了package-lock.json文件。关于这个package-lock.json是干什么用的,这个官方的说明文档。

简单来说,这个package-lock.json文件是npm用来“锁版本”的。这里所谓的锁版本是指当在项目拥有package-lock.json文件时,npm会根据其自动解析出包依赖,不会出现在不同的环境场景下,安装了不同的包版本。

在开始使用[email protected]的时候,执行完npm install后,有一个提示,

npm notice created a lockfile as package-lock.json. You should commit this file.

然后我就按照这句提示来做了。后来发现这玩意坑很多。所以我们现在想将这个package-lock.json文件放到.gitignore中,希望git仓库不再追踪这个文件。

方案

想做的事情已经很明确了,接下来我们来动手了。

首先明确一点,只要一个文件被git管理中(即被添加到git仓库中),那么无论是删除这个文件,还是将其添加到.gitignore文件中,都是没办法组织git继续对齐进行管理和追踪的。

所以,我们改写git仓库的历史记录。

step 1,

git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch package-lock.json' --prune-empty --tag-name-filter cat -- --all

我们使用了git filter-branch命令,对所有的分支上的commit执行额外的命令操作。这个操作就是git rm –cached –ignore-unmatch package-lock.json,忽略对package-lock.json文件的追踪,从git仓库中将其移除,并同时重写每一条记录。

step 2,

rm -rf .git/refs/original
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

现在历史记录中已经不包含对那个文件的引用了。不过reflog以及运行filter-branch时,git往.git/refs/original添加的一些refs中仍有对它的引用,因此需要将这些引用删除并对仓库进行repack操作。在进行repack前需要将所有对这些commits的引用去除。

Step 3,

git push origin master --force

将本地重写的git仓库强制推送到远程origin。

额外的问题

如果之前签出了远程分支,比如在很早的时候,通过git checkout –track origin/a签出了分支a到本地。那么在重写历史之后,可能本地a分支会丢失对远程分支remote/a的追踪。造成这种现象的原因是在重写记录时,将origin/a分支上的记录重写了,导致本地a分支与远程remote/a分支不再匹配。

此时,我们应该删除本地游离的分支a,然后从remote/a上重新签出分支a到本地。

另外还可能会出现一个问题,就是我们是针对master分支进行重写记录的,如果团队中其他成员在自己的本地有尚未合并进master分支的开发分支,那么可能会出现本地开发分支与远程分支游离的情况。此时我们需要针对这个开发分支再做一个重写历史记录。

参考链接

  • Removing sensitive data from a repository: (https://help.github.com/articles/removing-sensitive-data-from-a-repository/)

  • git-filter-branch – Rewrite branches: (https://git-scm.com/docs/git-filter-branch)

同一机器配置多个git密钥连接多个仓库

问题阐述

当有多个git账号的时候,比如一个github,用于自己进行一些开发活动,再来一个gitlab,一般是公司内部的git。这两者你的邮箱如果不同的话,就会涉及到一个问题,生成第二个git的key的时候会覆盖第一个的key,导致必然有一个用不了。

问题解决

我们可以在~/.ssh目录下新建一个config文件配置一下,就可以解决问题

具体步骤

  • 生成第一个ssh key(这里我用于github,用的gmail邮箱)
ssh-keygen -t rsa -C "[email protected]"

这里不要一路回传,让你选择在哪里选择存放key的时候写个名字,比如 id_rsa_github,之后的两个可以回车。

完成之后我们可以看到~/.ssh目录下多了两个文件

未分类

  • 生成第二个ssh key(这里我用于gitlab,用的是公司邮箱)
ssh-keygen -t rsa -C "[email protected]"

还是一样不要一路回车,在第一个对话的时候继续写个名字,比如 id_rsa_gitlab,之后的两个可以回车。

完成之后我们可以看到如2中图所标记,一样出现两个文件。(一个公钥一个私钥)

  • 打开ssh-agent

这里如果你用的github官方的bash,ssh-agent -s,如果是其他的,比如
msysgit,eval $(ssh-agent -s)

  • 添加私钥
ssh-add ~/.ssh/id_rsa_github
ssh-add ~/.ssh/id_rsa_gitlab
  • 创建并修改config文件

在windows下新建一个txt文本,然后将名字后缀一起改成config即可
在bash下的话直接touch config 即可。
添加一下内容

# gitlab
Host git.iboxpay.com
    HostName git.iboxpay.com  //这里填你们公司的git网址即可
    PreferredAuthentications publickey
    IdentityFile ~/.ssh/id_rsa_gitlab
    User zhangjun

# github
Host github.com
    HostName github.com
    PreferredAuthentications publickey
    IdentityFile ~/.ssh/id_rsa_github
    User ZJsnowman
  • 在github和gitlab上添加公钥即可,这里不再多说。

  • 测试

未分类

PS:如果到这里你没有成功的话,别急,教你解决问题的终极办法–debug

比如测试github,ssh -vT [email protected]

-v 是输出编译信息,然后根据编译信息自己去解决问题吧。就我自己来说一般是config里的host那块写错了。

补充一下

如果之前有设置全局用户名和邮箱的话,需要unset一下

git config --global --unset user.name
git config --global --unset user.email

然后在不同的仓库下设置局部的用户名和邮箱

比如在公司的repository下git config user.name “yourname” git config user.email “youremail” 在自己的github的仓库在执行刚刚的命令一遍即可。

这样就可以在不同的仓库,已不同的账号登录。

如何恢复丢弃的 git stash 数据

不要让 git 命令中的错误抹去你数天的工作

今天我的同事几乎失去了他在四天工作中所做的一切。由于不正确的 git 命令,他把保存在 stash[1] 中的更改删除了。在这悲伤的情节之后,我们试图寻找一种恢复他所做工作的方法,而且我们做到了!

首先警告一下:当你在实现一个大功能时,请将它分成小块并定期提交。长时间工作而不做提交并不是一个好主意。

现在我们已经搞定了那个错误,下面就演示一下怎样从 stash 中恢复误删的更改。

我用作示例的仓库中,只有一个源文件 “main.c”,如下所示:

未分类
Repository with one source file

它只有一次提交,即 “Initial commit”:

未分类
One commit

该文件的第一个版本是:

未分类
First version of the file

我将在文件中写一些代码。对于这个例子,我并不需要做什么大的改动,只需要有什么东西放进 stash 中即可,所以我们仅仅增加一行。“git diff” 的输出如下:

未分类
git-diff output

现在,假设我想从远程仓库中拉取一些新的更改,当时还不打算提交我自己的更改。于是,我决定先 stash 它,等拉取远程仓库中的更改后,再把我的更改恢复应用到主分支上。我执行下面的命令将我的更改移动到 stash 中:

git stash

使用命令 git stash list 查看 stash,在这里能看到我的更改:

未分类
Output of changes in our stash

我的代码已经在一个安全的地方,而且主分支目前是干净的(使用命令 git status 检查)。现在我只需要拉取远程仓库的更改,然后把我的更改恢复应用到主分支上,而且我也应该是这么做的。

但是我错误地执行了命令:

git stash drop

它删除了 stash,而不是执行了下面的命令:

git stash pop

这条命令会在从栈中删除 stash 之前应用它。如果我再次执行命令 git stash list,就能看到在没有从栈中将更改恢复到主分支的之前,我就删除了它。OMG!接下来怎么办?

好消息是:git 并没有删除包含了我的更改的对象,它只是移除了对它的引用。为了证明这一点,我使用命令 git fsck,它会验证数据库中对象的连接和有效性。这是我对该仓库执行了 git fsck 之后的输出:

未分类
Output after executing the git-fsck command on the repository

由于使用了参数 –unreachable,我让 git-fsck 显示出所有不可访问的对象。正如你看到的,它显示并没有不可访问的对象。而当我从 stash 中删除了我的更改之后,再次执行相同的指令,得到了一个不一样的输出:

未分类
Output after dropping changes on stash

现在有三个不可访问对象。那么哪一个才是我的更改呢?实际上,我不知道。我需要通过执行命令 git show 来搜索每一个对象。

未分类
Output after executing the git-show command

就是它!ID 号 95ccbd927ad4cd413ee2a28014c81454f4ede82c 对应了我的更改。现在我已经找到了丢失的更改,我可以恢复它。其中一种方法是将此 ID 取出来放进一个新的分支,或者直接提交它。如果你得到了你的更改对象的 ID 号,就可以决定以最好的方式,将更改再次恢复应用到主分支上。对于这个例子,我使用 git stash 将更改恢复到我的主分支上。

git stash apply 95ccbd927ad4cd413ee2a28014c81454f4ede82c

另外需要重点记住的是 git 会周期性地执行它的垃圾回收程序(gc),它执行之后,使用 git fsck 就不能再看到不可访问对象了。

搭建自己的git服务器

自己搭建git服务器主要有两个目的:

  • 可用来做一个团队的私有仓库,自己的服务器安全可控
  • 让代码自动同步到服务器,而不是每次用ftp,比较方便

系统 centos6 国外vps

步骤:

1. 安装git

一般服务器都自带git。如果嫌版本低,可以选择卸载重装。

2. 创建用户

groupadd git
adduser git -g git

3.创建authorized_keys文件

cd /home/git
mkdir .ssh
chmod 700 .ssh
touch .ssh/authorized_keys
chmod 600 .ssh/authorized_keys
cd /home
chown -R git:git git

4. 客户端创建密钥并上传

ssh-keygen -t rsa -C "your_email"

该命令会产生两个文件: id_rsa对应私钥,id_rsa.pub对应公钥。

将id_rsa.pub中的内容写到服务器的authorized_keys文件中。

如果有多个客户端,那么在authorized_keys文件中,一行保存一个客户端的公钥。

5. 创建git仓库

为了方便管理,所有的git仓库都置于同一目录下,假设为/data/gitrepo ,

cd /data
mkdir gitrepo
chown git:git gitrepo

接下来,创建我们的第一个git仓库:sample.git ,
服务器上的Git仓库通常都以.git结尾

cd gitrepo
git init --bare sample.git
// 修改所属用户
chown -R git:git sample.git

6. 克隆仓库到本地

$ git clone git@your-ip:/data/gitrepo/sample.git

7.禁用shell登录

安全性考虑git用户只提供git服务,不让登录到系统。

编辑/etc/passwd文件 找到类似下面的一行:

git:x:503:503::/home/git:/bin/bash

改为

git:x:503:503::/home/git:/sbin/nologin

8. 设置自动更新钩子

设置当有客户端push过来自动更新到指定目录(站点目录)

cd /data/girepo/sample.git/hooks
cp post-receive.sample post-receive
vi post-receive
//添加以下这句到文件
git --work-tree=/data/wwwroot/your-site-dir checkout -f
//保存
:wq

(完)

将代码从Git自动部署到容器

【编者的话】将您的代码从您的Git仓库转移到您的容器可能是一件痛苦的工作。本文介绍了如何实现自动化部署。

有许多方法可以将您的源代码从Git 仓库部署到容器中,包括整个容器的重新部署、通过卷的即时重新部署或“git clone”方法。然而,当涉及到这一过程的自动化和持续部署时,许多开发人员可能会面临其中的复杂性,因为他们需要知道如何正确地将所有应用程序组件与所需的互连点结合起来。

尤其是在容器里,必须管理所构建的堆栈映像来处理CI/CD管道引起的额外复杂性。如果在操作系统、应用服务器堆栈或其依赖项中进行频繁的提交,那么整个容器重新部署可能不是最好的方法。

未分类

为了简化部署自动化,Jelastic准备了一种特殊专用的Git-Push-Deploy包,用于将代码交付到初步构建的容器映像中。这个包实现了许多配置,以便在您的Git应用程序源存储库中自动部署提交的更改,使其能够在最小延迟的情况下进行进一步的测试。

Git-Push-Deploy 细节

Git-Push-Deploy包可以与GitHub和GitLab存储库集成。它是为在Java、PHP、Ruby和Node.js和Python应用源代码中自动提供更新而开发的。可以应用于以下认证的模板:

  • Java – Tomcat 6/7/8/9, TomEE, GlassFish 3/4, Jetty 6/8/9, WildFly 8/9/10, JBoss AS 7, Spring Boot 1.x
  • PHP – Apache 2.4, NGINX 1.10
  • Ruby – Apache 2.4, NGINX 1.10
  • Node.js – Node.js 0.x-6.x
  • Python – Apache 2.4

工作流依赖于项目中使用的编程语言:

  • 对于基于Java的项目,该包将创建一个单独的环境,其中包含Maven构建节点,它将负责与远程Git存储库的交互,从而触发您的应用程序构建及其部署到应用程序服务器。
  • 对于PHP / Ruby /Node.js/python应用中,该包为项目的部署直接在Web服务器的ROOT context上设置了一个管道(这里,认为 Ruby应用程序服务器提供的是一种部署模式,而不是dashboard的context,尽管实际项目位置是相同的)。

这个部署自动化包与4.9.5版本的Jelastic PaaS以及更高版本兼容。要查看和比较可用的管理平台和特定的Jelastic版本,请参考Jelastic Cloud Union目录。

库预配置

对于特定的附加安装,您需要为您的Git帐户提供一个个人的API Token。这可以使包为对应的存储源设置一个webhook,在每次更改代码并使其生效时,来初始化重新部署应用。

我们来生成一个。根据你所使用的Git VCS,GitHub或者GitLab对应以下指示进行操作。

在GitHub上生成Access Token

要为您的GitHub帐户获取您的personal access token,请访问Settings > Personal access tokens然后点 Generate new token按钮。

未分类

在打开的页面中,选定Token description,并选择repo和admin:repohook,在页面底部单击Generate token。

未分类

将生成的access token复制保存起来(因为离开这个页面之后,就不能再被查看)。

未分类

完成这一步之后,开始继续安装Git-Push-Deploy包。

在GitLab上生成Access Token

要在GitLab生成一个personal access token,进入您的帐户settings并切换到Access Tokens。

在这里,指定可选的token名称,它的到期日期(可以留空),并勾选api权限范围。

未分类

点击 Create Personal Access Token按钮。

在打开的页面中,复制并临时将您的访问令牌值存储在其他地方(因为离开本页后您将无法再次看到它)。

未分类

现在,您已经准备好继续安装Git-Push-Deploy包了。

Java项目的额外预配置

如果运行一个基于Java的项目,您需要通过在其结构中添加一个特殊的项目对象模型(POM)文件来开始与Maven构建节点进行适当的交互。
所以,创建一个pom.xml文件在项目源的根目录中,然后必须添加下面的内容:

<project>    
<modelVersion>4.0.0</modelVersion>    
<groupId>com.mycompany.app</groupId>    
<artifactId>my-app</artifactId>    
<version>1.0</version>    
<packaging>war</packaging>    
<build>        
<finalName>${project.artifactId}</finalName>   
</build> 
</project>

可选值:

  • groupId -项目组(例如公司名称)。
  • artifactId-项目的名称。
  • version -应用版本。

其余的参数都应该保持不变。您可以在我们的示例中查看如何配置。

安装Git-Push-Deploy包

Git-Push-Deploy包是一个附加组件,因此它只能安装在一个环境的顶部。我们已经准备了两个独立的环境,分别使用Tomcat和apache-php应用程序服务器来展示不同编程语言的工作流。

如果你打算使用以前创建的环境,请注意这个包将覆盖部署到根目录的应用程序。因此,为了保持已经部署的应用程序,将其移动到用户目录。我们建议创建一个新的环境,然后进行接下来的安装:

1、单击dashboard顶部窗格中的Import按钮,在打开的URL选项卡中插入一个manifest.jps链接:

https://github.com/jelastic-jps/git-push-deploy/blob/master/manifest.jps

未分类

单击Import 继续。

2、在打开的框架中,指定关于源和目标环境的详细信息:

  • Git Repo URL – HTTPS连接到你的应用程序的“repo“(或者 .git或普通视图)。您可以fork我们的示例Hello World应用程序来测试流。
  • Branch – 项目分支。
  • User – 输入您的Git登录帐户。
  • Token –指定您之前为webhook生成创建的 access token。
  • Environment name – 选择要部署应用程序的环境。
  • Nodes – 应用程序服务器名(在选择环境时自动获取)。

未分类

单击“Install ”继续。

3、等一分钟让Jelastic从GitHub获取应用程序资源,配置webhook以进行接下来的部署。

未分类

安装完成后点击“Close“关闭通知框。

4、根据项目类型的不同,会有如下结果:

  • 对于基于Java的基础设施,您将在dashboard中看到一个新的环境,其中包含Maven构建节点;它将在每次更新源代码时,在Web服务器上构建和部署应用程序到Root context下。

未分类

请注意,Maven可能需要花费一些时间来编译一个项目(尽管包安装本身已经完成了),因此您需要等待几分钟才能启动它。这个操作的当前进展可以通过Maven的vcs_update 日志文件实时跟踪。

未分类

  • 对于基于PHP的基础设施(以及其他受支持的语言),您的应用程序将直接部署到所选的服务器Root context上。

请注意,Ruby应用程序服务器的类似项目部分提供了关于使用的部署模式(默认情况下开发)的信息,而不是context,同时实际的应用程序位置也引用了服务器root。

要启动应用程序,请单击web服务器旁边的Open in browser。

未分类

就是这样!现在,您的应用程序的新版本在每次提交到源端时都会自动地发送到应用程序服务器。

不同服务栈的重新部署策略

下面的表列出了在接收到更新后的代码后不同应用服务器的反应。

未分类

为了消除可能的应用程序停机时间,可以使用重新启动更新策略,将其扩展到多个容器中。在这种情况下,所需的更新将按顺序应用到实例中,默认情况下延迟30秒。

通过Git上来测试自动部署

现在让我们来看看这个过程是如何工作的。对源端的代码做一些细微的调整,确保一切都是自动化的:

1、单击您的项目源中的某个项目的Edit this file,并Commit changes——例如,我们将修改HelloWorld启动页面上的文本。

未分类

2、因此,将触发适当的webhook来将所做的更改部署到您的主机环境中——请参考存储库Settings > Webhooks部分以了解详细信息。

未分类

单击此字符串后,您将看到由webhook发起的最近交付的列表,以及它们执行的结果。

3、最后一个检查,返回到您的应用程序页面并刷新(同时记住,Maven可能需要额外的一分钟来构建和部署您的基于Java的项目)。

未分类

就是这样!正如您所看到的,这些修改成功地应用了,因此这个方案按照我们预期的方式工作。

只需更新代码,按照通常的方法进行提交,所有的更改都会自动地推送到您的Jelastic环境中。不需要在进程之间切换,也不需要手动更新,从而消除人为错误,并加速应用程序的上线时间。

原文链接: https://dzone.com/articles/deploy-code-to-containers-from-git-automatically-1 (翻译:edge_dawn)

git回退到指定的版本

我的开发分支合并了 develop ,发现代码有问题,现在需要回滚到合并之前的版本。

使用 git log 查看下最近的提交,如果有如下的提交

G1 - G2 - G3 - B1 - B2 - B3
G1 - G2 - G3 - B1 - B2 - B3
                       -- HEAD
                    ------ HEAD~1
                 ---------- HEAD~2
              -------------- HEAD~3

这里分两种情况,这里要回退 B1 ~ B3

  • 没有提交到服务器

可以使用如下中的一种挨个回滚

git reset --hard B1 B2 B3
git reset --hard HEAD HEAD~1 HEAD~2

注意上面的 B1 B2 B3 都是指的提交的ID

  • 已经提交到服务器

如果代码已经提交了,最好使用 revert

git revert --no-commit B1 B2 B3
git revert --no-commit HEAD HEAD~1 HEAD~2
git revert --no-commit HEAD~2^..HEAD

这里 –no-commit 告诉 git 执行还原,但是不要自动提交

<rev1>..<rev2> 两个点 .. 表示范围

我这里的由于是要回退一个 merge过的提交,需要指定 -m 参数,可以使用 man git-revert 查看具体的使用方法。

man git-revert
git revert --no-commit -m 1 具体的提交ID

可以参考 回退一个错误的merge(http://web.mit.edu/jhawk/mnt/spo/git/www/howto/revert-a-faulty-merge.html)

忽略git已经跟踪的文件或者目录

大家都知道, 在git项目下想要忽略某个文件,只需要在gitignore文件中添加此文件,但是有时候却不起作用,这是为什么呢?

因为如果第一次提交的时候,没有在gitignore文件中添加忽略文件,那么这些文件(目录也是文件)就会被git跟踪,push的时候也会被推送到远程。被跟踪的文件,gitignore都会失效,所以最好就是一开始在commit之前先添加到gitignore中。

如果文件已经被跟踪且被推送到远程,可以按照下面方法解决:

rm -rf 文件
git rm -r --cached 要忽略的文件

更新.gitignore文件,添加要忽略的文件

git add -A (添加所有)
git push origin 分支

如果同名的文件过多,如:.class 文件被提交了,那么如果这样一个一个显然效率太低,可以按照下面方法操作

find . -iname 文件名 -exec rm -rf {};
  • 重复上面的步骤,文件名替换为下一个要删除的文件名

  • 修改gitignore,添加忽略文件

git rm -r --cached 要忽略的文件
git add -A
git push origin 分支

20条实用的git命令介绍

个人总结出的一些实用的 git 命令,分享给大家。

git config --global color.ui true 

让 git 命令默认使用彩色输出。 这条命令在 git 2 之后已经成为默认配置,但如果你还在用比较老的版本(例如 CentOS 上的默认的 git 版本),建议把这项配置加上去。

git branch -av 

显示所有本地即远程分支,并显示最后提交的 Commit 信息。如果不加参数,则只会显示所有本地分支的名字。

git checkout -b <NAME> [<START POINT>] 

创建,并切换到新分支。git branch 只会创建分支而不会切换到新分支,可以用它备份当前分支。

git tag -L <PATTERN> 

列出所有符合条件的标签。如果你的项目严格按照 Major.Minor.Update 作为版本名称,那么这条命令就非常有用。它可以直接列出来当前版本下有那些小的 bugfix 版本。当然如果你非要 grep 一下我也没意见。

git diff -w 

显示未提交的更改,忽略空格。人们往往注重实际代码的改变而不注重缩进的变化。如果某个文件有大量缩进改变,-w 这个参数就非常有用。

git commit --amend 

修补前一次提交。相当于撤销前一次提交,做更改后重新提交。这也是一条非常实用的命令。当你发现前一次提交有一些小问题的时候(比如说漏提交了新创建的文件,或者有一些小的拼写错误),可以用这条命令修正前一次提交。它对 merge 提交友好,而且可以用于修正前一次提交的 message 信息。需要注意的是:如果你已经推送了前一次提交,amend 之后需要强推,这一点需要注意。

git log --graph --oneline --no-merge 

显示当前仓库的提交历史。git log 大家都用过,但真正研究过 git log 后面参数的人可能就不多。git log 后面可以接很多实用的参数,示例中的让提交历史以单行模式显示、显示提交历史树并删除 merge 提交。

git log -p --follow --stat -- <PATH> 

显示某个文件的提交历史。在 git 中,– 后接文件路径就代表对单个文件的操作。-p 可以显示具体修改的行,–follow 可以跟踪文件的移动和重命名,–stat 用于显示添加、删除行的数量。

git config --global alias.xx "<COMMAND>" 

给某 git 命令创建别名。对于一些比较长的命令,可以创建别名。以后只需要 git xx 即可执行 COMMAND 这条命令

git pull --rebase, --rebase 

可以简写为 -r
使用 git rebase 代替 git merge 执行 pull 操作。git pull –rebase 可以构造出非常整齐的提交历史树,强迫症的福利。git 的官方文档一再提醒这是个危险操作,因为它会修改你的代码提交历史。git rebase 的本质是撤销指定的提交,然后以指定的方式重新提交他们。git pull -r 就相当于首先撤销没有推送到远端的 commit,将远程代码覆盖到本地之后,重新提交所有之前撤销的 commit。与 git merge 不同,当有冲突产生时,git rebase 不会为你的 merge 操作生成一个新的提交。所以一旦 git pull –rebase 执行完毕就无法撤销。

git config --global pull.rebase true 

默认使用 git rebase 代替 git merge 执行 pull 操作。git 提供了一系列配置 git pull –rebase 操作:branch..rebase、branch.autosetuprebase。这条是最简单的全局配置项。尽管配置了默认使用 rebase,你可以使用 –merge 开关强制使用 git merge 执行 git pull。

git rebase -i 

交互式变基操作。git 中最强大的修改提交历史的操作,当然也是最危险的操作。它可以让你修改 commit 说明、让几个 commit 合并、交换 commit、删除 commit,甚至在提交某 commit 前执行一段 shell 命令。非常有用、非常强大、同时也是极其危险的操作。强烈建议在执行 git rebase -i 之前先使用 git branch 备份当前分支。

git push <remote> [<commit>]:<branch> 

推送指定的 commit 到远程。有时候你某个工作做到一半,然后来了一个bug要修。当然最好的做法是基于最新的远端分支新开一个分支,基于这个分支开发。但是如果你忘了新开分支,直接把代码提交到了当前分支怎么办?在你需要 push 的分支之前又有别的不需要的 commit。这时就可以先用 git rebase 交换 commit 的顺序,然后推送单个提交。如果你写了冒号但是不写 commit 号,就会变成删除某个远端分支。这是完全不同的而且可能有危险操作,需要注意。

git blame <PATH> [-L <M,N>] 

逐行检查某文件的提交人、提交时间和 commit 号。blame 的中文意思是追责,大家顾名思义,撕逼甩锅时用。 -L 可以指定要检查的行号。

git stash [push] [-u] 

贮存当前工作区的更改。经常有这样的事情发生,当你正在进行项目中某一部分的工作,里面的东西处于一个比较杂乱的状态,而你想转到其他分支上进行一些工作。问题是你还不想提交这些代码,这是就可以用 git stash 命令将未提交的更改临时保存到一个贮存项中。-u 表示将未加入版本管理的文件也保存(比如新建的一些文件)

git stash apply <INDEX>、git stash pop <INDEX> 

将最后一次保存的(或指定的某个)贮存项应用至当前工作区。与前面的 git stash [push] 配套使用。 apply 与 pop 的区别是:pop 会在应用更改的同时把所应用的贮存项删除,而 apply 不会。

git cherry-pick -x <COMMIT>.. 

摘取 某些提交。即把另一个本地分支的 commit 应用到当前分支。如果我们同时维护多个分支,这个操作就很有用。比如说你在主干 master 分支上修复了一个 bug,然后你想把这个修复应用到一个旧版本的分支上,但是你又不想把其他 master 分支的新功能拉进来,这时就可以用 cherry-pick。-x:在提交记录中添加一行 (cherry picked from commit ),让两个提交关联起来。

git revert <COMMIT> 

回滚某个提交。即提交某个 COMMIT 的反提交。最快修复某个 bug 的方式就是把引入 bug 的代码干掉。注意干掉代码不代表就要用 git rebase -i 把提交本身也干掉。

git grep <PATTERN> 

在当前加入版本管理的文件中全文检索某字符串。类似于 grep 操作,但是会忽略不需要的(加入 .gitignore 的)文件,非常实用的命令。

git bisect 

实用二分查找的方式定位引入问题的提交。快速定位 bug 的方式。在找不到 bug 出现的原因时,不妨用 git bisect 将 bug 先锁定到某个 commit 上。

brew install tig 

美化 git 输出,重度终端用户的福利。这个其实是工具安利了,它用图形化的方式(ncurses)美化 git 输出,谁用谁知道。